@deque/axe-auth 1.1.0-next.97bcb8e6 → 1.1.0-next.a50d07c6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -7
- package/credits.json +53 -0
- package/dist/cli/commonArgs.d.ts +35 -0
- package/dist/cli/commonArgs.help.d.ts +2 -0
- package/dist/cli/commonArgs.help.js +20 -0
- package/dist/cli/commonArgs.js +63 -0
- package/dist/cli/confirm.d.ts +17 -0
- package/dist/cli/confirm.js +53 -0
- package/dist/cli/errors.d.ts +13 -0
- package/dist/cli/errors.js +30 -0
- package/dist/cli/testUtils.d.ts +52 -0
- package/dist/cli/testUtils.js +100 -0
- package/dist/cli/types.d.ts +39 -0
- package/dist/cli/types.js +2 -0
- package/dist/commands/login.d.ts +41 -0
- package/dist/commands/login.help.d.ts +2 -0
- package/dist/commands/login.help.js +41 -0
- package/dist/commands/login.js +108 -0
- package/dist/commands/logout.d.ts +24 -0
- package/dist/commands/logout.help.d.ts +2 -0
- package/dist/commands/logout.help.js +38 -0
- package/dist/commands/logout.js +68 -0
- package/dist/commands/token.d.ts +21 -0
- package/dist/commands/token.help.d.ts +2 -0
- package/dist/commands/token.help.js +41 -0
- package/dist/commands/token.js +40 -0
- package/dist/index.js +119 -27
- package/dist/oauth/authorizationURL.d.ts +24 -0
- package/dist/oauth/authorizationURL.js +48 -0
- package/dist/oauth/authorize.d.ts +53 -0
- package/dist/oauth/authorize.js +117 -0
- package/dist/oauth/callbackServer.d.ts +23 -0
- package/dist/oauth/callbackServer.js +234 -0
- package/dist/oauth/discoverOIDC.d.ts +33 -0
- package/dist/oauth/discoverOIDC.js +144 -0
- package/dist/oauth/discoverSSOConfig.d.ts +37 -0
- package/dist/oauth/discoverSSOConfig.js +105 -0
- package/dist/oauth/errors.d.ts +77 -0
- package/dist/oauth/errors.js +48 -0
- package/dist/oauth/getValidAccessToken.d.ts +54 -0
- package/dist/oauth/getValidAccessToken.js +131 -0
- package/dist/oauth/index.d.ts +16 -0
- package/dist/oauth/index.js +19 -0
- package/dist/oauth/issuerURL.d.ts +22 -0
- package/dist/oauth/issuerURL.js +38 -0
- package/dist/oauth/keyringBinding.d.ts +22 -0
- package/dist/oauth/keyringBinding.js +41 -0
- package/dist/oauth/logo.generated.d.ts +1 -0
- package/dist/oauth/logo.generated.js +7 -0
- package/dist/oauth/openBrowser.d.ts +30 -0
- package/dist/oauth/openBrowser.js +95 -0
- package/dist/oauth/pkce.d.ts +17 -0
- package/dist/oauth/pkce.js +43 -0
- package/dist/oauth/predicates.d.ts +7 -0
- package/dist/oauth/predicates.js +15 -0
- package/dist/oauth/refreshTokens.d.ts +30 -0
- package/dist/oauth/refreshTokens.js +60 -0
- package/dist/oauth/renderHtml.d.ts +9 -0
- package/dist/oauth/renderHtml.js +60 -0
- package/dist/oauth/revokeToken.d.ts +28 -0
- package/dist/oauth/revokeToken.js +63 -0
- package/dist/oauth/testUtils.d.ts +35 -0
- package/dist/oauth/testUtils.js +61 -0
- package/dist/oauth/tokenExchange.d.ts +26 -0
- package/dist/oauth/tokenExchange.js +44 -0
- package/dist/oauth/tokenResponse.d.ts +22 -0
- package/dist/oauth/tokenResponse.js +101 -0
- package/dist/oauth/tokenStore.d.ts +183 -0
- package/dist/oauth/tokenStore.js +560 -0
- package/dist/userAgent.d.ts +12 -0
- package/dist/userAgent.js +18 -0
- package/docs/architecture.md +201 -0
- package/docs/callback-page.md +24 -0
- package/docs/callback-server.md +21 -0
- package/docs/oauth-flow.md +15 -0
- package/package.json +20 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverSSOConfig = discoverSSOConfig;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const predicates_1 = require("./predicates");
|
|
6
|
+
const userAgent_1 = require("../userAgent");
|
|
7
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
8
|
+
function assertSecureServerURL(serverURL, allowInsecure) {
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(serverURL);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server URL is not a valid URL: ${serverURL}`);
|
|
15
|
+
}
|
|
16
|
+
if (parsed.protocol === "https:")
|
|
17
|
+
return parsed;
|
|
18
|
+
if (parsed.protocol === "http:") {
|
|
19
|
+
const hostname = parsed.host.toLowerCase().replace(/:\d+$/, "");
|
|
20
|
+
if (LOOPBACK_HOSTS.has(hostname))
|
|
21
|
+
return parsed;
|
|
22
|
+
if (allowInsecure)
|
|
23
|
+
return parsed;
|
|
24
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `Refusing to use axe server URL over http:// against non-loopback host ${parsed.host}. Use https:// or pass --allow-insecure-issuer to override (only do this on trusted networks).`);
|
|
25
|
+
}
|
|
26
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `Unsupported axe server URL scheme '${parsed.protocol}'; expected https: or http: (loopback only).`);
|
|
27
|
+
}
|
|
28
|
+
function buildSSOConfigURL(parsed) {
|
|
29
|
+
const copy = new URL(parsed.toString());
|
|
30
|
+
copy.search = "";
|
|
31
|
+
copy.hash = "";
|
|
32
|
+
copy.pathname = `${copy.pathname.replace(/\/+$/, "")}/api/sso-config`;
|
|
33
|
+
return copy.toString();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fetches and parses the axe server's `/api/sso-config` discovery
|
|
37
|
+
* endpoint. Used by `axe-auth login` to derive the OAuth issuer URL,
|
|
38
|
+
* realm, and CLI-specific client ID from the axe server URL the user
|
|
39
|
+
* supplied (or the SaaS prod default), so users no longer have to know
|
|
40
|
+
* the underlying Keycloak coordinates.
|
|
41
|
+
*
|
|
42
|
+
* Distinguishes three failure shapes for the operator-relevant cases:
|
|
43
|
+
*
|
|
44
|
+
* - `mcpClientId` field absent: the axe server deployment predates the
|
|
45
|
+
* field entirely. Surfaces as "needs upgrading".
|
|
46
|
+
* - `mcpClientId` is `null`: the axe server version supports the field
|
|
47
|
+
* but the operator has not configured `KEYCLOAK_MCP_PUBLIC_CLIENT_ID`.
|
|
48
|
+
* Surfaces as "ask the operator to configure".
|
|
49
|
+
* - any non-empty string: returned as-is.
|
|
50
|
+
*
|
|
51
|
+
* Other failure modes (unreachable, non-2xx, malformed JSON, missing
|
|
52
|
+
* `url` / `realm`) all map to `DISCOVERY_FAILED` with a descriptive
|
|
53
|
+
* message. The caller is expected to surface these errors verbatim.
|
|
54
|
+
*/
|
|
55
|
+
async function discoverSSOConfig(serverURL, options = {}) {
|
|
56
|
+
const allowInsecure = options.allowInsecure ?? false;
|
|
57
|
+
const parsed = assertSecureServerURL(serverURL, allowInsecure);
|
|
58
|
+
const url = buildSSOConfigURL(parsed);
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(url, {
|
|
62
|
+
headers: { "User-Agent": userAgent_1.USER_AGENT },
|
|
63
|
+
signal: options.signal,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (cause) {
|
|
67
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `Could not reach the axe server at ${url}. Check the --server URL and your network connection.`, { cause });
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server at ${url} responded with HTTP ${response.status}. Check the --server URL.`);
|
|
71
|
+
}
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = await response.json();
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server at ${url} did not return valid JSON.`, { cause });
|
|
78
|
+
}
|
|
79
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
80
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server at ${url} returned a non-object response body.`);
|
|
81
|
+
}
|
|
82
|
+
const raw = body;
|
|
83
|
+
const missing = [];
|
|
84
|
+
if (!(0, predicates_1.isNonEmptyString)(raw.url))
|
|
85
|
+
missing.push("url");
|
|
86
|
+
if (!(0, predicates_1.isNonEmptyString)(raw.realm))
|
|
87
|
+
missing.push("realm");
|
|
88
|
+
if (missing.length > 0) {
|
|
89
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `${url} is missing required field(s): ${missing.join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
if (!("mcpClientId" in raw)) {
|
|
92
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server at ${serverURL} does not advertise OAuth-based MCP authentication. The deployment may need to be upgraded to a version that supports the axe-auth CLI.`);
|
|
93
|
+
}
|
|
94
|
+
if (raw.mcpClientId === null) {
|
|
95
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server at ${serverURL} has not been configured for OAuth-based MCP authentication. Ask your operator to set the KEYCLOAK_MCP_PUBLIC_CLIENT_ID environment variable on the axe server.`);
|
|
96
|
+
}
|
|
97
|
+
if (!(0, predicates_1.isNonEmptyString)(raw.mcpClientId)) {
|
|
98
|
+
throw new errors_1.OAuthFlowError("DISCOVERY_FAILED", `axe server at ${serverURL} returned a malformed mcpClientId (expected a non-empty string).`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
url: raw.url,
|
|
102
|
+
realm: raw.realm,
|
|
103
|
+
mcpClientId: raw.mcpClientId,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Error codes raised by the loopback callback server. */
|
|
2
|
+
export type OAuthCallbackErrorCode =
|
|
3
|
+
/** No callback arrived within `timeoutMs`; a retry is reasonable. */
|
|
4
|
+
"TIMEOUT"
|
|
5
|
+
/** Callback `state` did not match `expectedState`. Surface a generic failure — do NOT echo the expected value. */
|
|
6
|
+
| "STATE_MISMATCH"
|
|
7
|
+
/** Callback carried `?error=...`. `details` holds `error` and optionally `error_description` for display. */
|
|
8
|
+
| "PROVIDER_ERROR"
|
|
9
|
+
/** Callback had no `code` parameter; treat as a malformed response. */
|
|
10
|
+
| "MISSING_CODE"
|
|
11
|
+
/** Could not bind the loopback port. No retry — environment likely blocked. */
|
|
12
|
+
| "BIND_FAILED"
|
|
13
|
+
/** Caller's `AbortSignal` fired. Expected; no user-facing message. */
|
|
14
|
+
| "ABORTED";
|
|
15
|
+
/** Options for `OAuthCallbackError`. */
|
|
16
|
+
export interface OAuthCallbackErrorOptions {
|
|
17
|
+
/** Structured metadata for callers that want to surface specific fields. */
|
|
18
|
+
details?: Record<string, string>;
|
|
19
|
+
/** Underlying error that triggered this failure. */
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Error raised by the loopback callback server when the authorization
|
|
24
|
+
* response cannot be consumed (timeout, state mismatch, provider error,
|
|
25
|
+
* malformed response, bind failure, or caller abort).
|
|
26
|
+
*/
|
|
27
|
+
export declare class OAuthCallbackError extends Error {
|
|
28
|
+
/** Discriminator for programmatic handling. */
|
|
29
|
+
readonly code: OAuthCallbackErrorCode;
|
|
30
|
+
/** Structured metadata carried alongside the error, if any. */
|
|
31
|
+
readonly details?: Record<string, string>;
|
|
32
|
+
constructor(code: OAuthCallbackErrorCode, message: string, options?: OAuthCallbackErrorOptions);
|
|
33
|
+
}
|
|
34
|
+
/** Error codes raised by the OAuth flow orchestrator and its helpers. */
|
|
35
|
+
export type OAuthFlowErrorCode =
|
|
36
|
+
/** OIDC discovery could not reach or parse the authorization server. No browser was opened. */
|
|
37
|
+
"DISCOVERY_FAILED"
|
|
38
|
+
/** Could not launch the system browser. User should be told to open the URL manually. */
|
|
39
|
+
| "BROWSER_LAUNCH_FAILED"
|
|
40
|
+
/** Authorization code → token exchange was rejected by the authorization server. */
|
|
41
|
+
| "TOKEN_EXCHANGE_FAILED"
|
|
42
|
+
/** System keychain is unavailable (e.g. no D-Bus secret service on Linux). */
|
|
43
|
+
| "KEYRING_UNAVAILABLE"
|
|
44
|
+
/** OAuth blob is too large for the OS keystore (Windows Credential Manager: 2560 UTF-16 chars per entry, MAX_CHUNKS chunks max). The keystore itself is healthy; the IDP is issuing tokens with too many claims. */
|
|
45
|
+
| "TOKEN_TOO_LARGE"
|
|
46
|
+
/** Authorization endpoint returned by discovery cannot be used (e.g. already carries an OAuth-required param). Server misconfiguration. */
|
|
47
|
+
| "INVALID_AUTHORIZATION_ENDPOINT"
|
|
48
|
+
/** No usable stored credentials; the user needs to run `login` to re-authenticate. Covers empty / corrupt / version-mismatched store and refresh tokens the authorization server has revoked. */
|
|
49
|
+
| "NOT_AUTHENTICATED";
|
|
50
|
+
/** Options for `OAuthFlowError`. */
|
|
51
|
+
export interface OAuthFlowErrorOptions {
|
|
52
|
+
/** Structured metadata for callers that want to surface specific fields. */
|
|
53
|
+
details?: Record<string, string>;
|
|
54
|
+
/** Underlying error that triggered this failure. */
|
|
55
|
+
cause?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Error raised by anything in the OAuth flow outside the callback
|
|
59
|
+
* server itself: OIDC discovery, browser launch, token exchange, or
|
|
60
|
+
* keychain access.
|
|
61
|
+
*
|
|
62
|
+
* **Logging note.** For code `TOKEN_EXCHANGE_FAILED`, `message` may
|
|
63
|
+
* include the authorization server's `error_description`, which is
|
|
64
|
+
* free-form text the server controls and could plausibly contain
|
|
65
|
+
* user-identifying or otherwise sensitive information. Prefer
|
|
66
|
+
* structured logging via the discriminated `code` and the explicit
|
|
67
|
+
* `details` fields; avoid echoing the raw `message` (or the result
|
|
68
|
+
* of `console.error(err)`, which includes it) into shared log sinks
|
|
69
|
+
* without a redaction step.
|
|
70
|
+
*/
|
|
71
|
+
export declare class OAuthFlowError extends Error {
|
|
72
|
+
/** Discriminator for programmatic handling. */
|
|
73
|
+
readonly code: OAuthFlowErrorCode;
|
|
74
|
+
/** Structured metadata carried alongside the error, if any. */
|
|
75
|
+
readonly details?: Record<string, string>;
|
|
76
|
+
constructor(code: OAuthFlowErrorCode, message: string, options?: OAuthFlowErrorOptions);
|
|
77
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OAuthFlowError = exports.OAuthCallbackError = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Error raised by the loopback callback server when the authorization
|
|
6
|
+
* response cannot be consumed (timeout, state mismatch, provider error,
|
|
7
|
+
* malformed response, bind failure, or caller abort).
|
|
8
|
+
*/
|
|
9
|
+
class OAuthCallbackError extends Error {
|
|
10
|
+
/** Discriminator for programmatic handling. */
|
|
11
|
+
code;
|
|
12
|
+
/** Structured metadata carried alongside the error, if any. */
|
|
13
|
+
details;
|
|
14
|
+
constructor(code, message, options = {}) {
|
|
15
|
+
super(message, { cause: options.cause });
|
|
16
|
+
this.name = "OAuthCallbackError";
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.details = options.details;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.OAuthCallbackError = OAuthCallbackError;
|
|
22
|
+
/**
|
|
23
|
+
* Error raised by anything in the OAuth flow outside the callback
|
|
24
|
+
* server itself: OIDC discovery, browser launch, token exchange, or
|
|
25
|
+
* keychain access.
|
|
26
|
+
*
|
|
27
|
+
* **Logging note.** For code `TOKEN_EXCHANGE_FAILED`, `message` may
|
|
28
|
+
* include the authorization server's `error_description`, which is
|
|
29
|
+
* free-form text the server controls and could plausibly contain
|
|
30
|
+
* user-identifying or otherwise sensitive information. Prefer
|
|
31
|
+
* structured logging via the discriminated `code` and the explicit
|
|
32
|
+
* `details` fields; avoid echoing the raw `message` (or the result
|
|
33
|
+
* of `console.error(err)`, which includes it) into shared log sinks
|
|
34
|
+
* without a redaction step.
|
|
35
|
+
*/
|
|
36
|
+
class OAuthFlowError extends Error {
|
|
37
|
+
/** Discriminator for programmatic handling. */
|
|
38
|
+
code;
|
|
39
|
+
/** Structured metadata carried alongside the error, if any. */
|
|
40
|
+
details;
|
|
41
|
+
constructor(code, message, options = {}) {
|
|
42
|
+
super(message, { cause: options.cause });
|
|
43
|
+
this.name = "OAuthFlowError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.details = options.details;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.OAuthFlowError = OAuthFlowError;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type LoadResult, type TokenStore } from "./tokenStore";
|
|
2
|
+
/** Options for `getValidAccessToken`. */
|
|
3
|
+
export interface GetValidAccessTokenOptions {
|
|
4
|
+
/** OIDC issuer URL. Must match the stored entry's `issuerURL`; mismatch throws NOT_AUTHENTICATED. */
|
|
5
|
+
issuerURL: string;
|
|
6
|
+
/** OAuth client identifier. Must match the stored entry's `clientId`; same mismatch behavior as `issuerURL`. */
|
|
7
|
+
clientId: string;
|
|
8
|
+
/**
|
|
9
|
+
* How close to expiry preemptive refresh kicks in, in ms. Default
|
|
10
|
+
* 60_000. Buffer covers clock skew vs. the server. Assumes
|
|
11
|
+
* access-token TTL ≫ this; otherwise every call refreshes.
|
|
12
|
+
*/
|
|
13
|
+
expiryBufferMs?: number;
|
|
14
|
+
/** Override for the token store. */
|
|
15
|
+
tokenStore?: TokenStore;
|
|
16
|
+
/** Pre-loaded `tokenStore.load()` result so the dispatcher's keychain read isn't repeated. */
|
|
17
|
+
loadedEntry?: LoadResult;
|
|
18
|
+
/** Aborts discovery + the refresh POST when fired. */
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
/** Forwarded to discovery; permits non-loopback http. */
|
|
21
|
+
allowInsecureIssuer?: boolean;
|
|
22
|
+
/** Called for soft warnings (e.g. rotated tokens couldn't be persisted — see HAZARD note in the body). Default prints to stderr only when stderr is a TTY. */
|
|
23
|
+
onWarning?: (message: string) => void;
|
|
24
|
+
/** Source of `now`. Defaults to `Date.now`. Injected for test determinism. */
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Returns a currently-valid access token string for the given issuer,
|
|
29
|
+
* refreshing via the stored refresh token if the cached access token
|
|
30
|
+
* is within `expiryBufferMs` of expiring (or already expired).
|
|
31
|
+
*
|
|
32
|
+
* Throws `OAuthFlowError("NOT_AUTHENTICATED", ...)` when the user
|
|
33
|
+
* must re-run `axe-auth login` — covers an empty / corrupt /
|
|
34
|
+
* version-mismatched store, an expired access token with no refresh
|
|
35
|
+
* token to rotate with, and a refresh attempt rejected with
|
|
36
|
+
* `invalid_grant` (which also clears the stored tokens).
|
|
37
|
+
*
|
|
38
|
+
* Throws `OAuthFlowError("TOKEN_EXCHANGE_FAILED", ...)` for transient
|
|
39
|
+
* failures during refresh (network errors, 5xx, malformed responses)
|
|
40
|
+
* and leaves the stored tokens intact so a retry is possible.
|
|
41
|
+
*
|
|
42
|
+
* Throws `OAuthFlowError("DISCOVERY_FAILED", ...)` when the issuer
|
|
43
|
+
* URL cannot be reached or parsed at refresh time.
|
|
44
|
+
*
|
|
45
|
+
* **Concurrency note.** Not safe against parallel invocations for
|
|
46
|
+
* the same issuer. Keycloak rotates refresh tokens by default; if
|
|
47
|
+
* two parallel calls both land on the refresh path, only one
|
|
48
|
+
* winner's rotated refresh token will be persisted and the loser's
|
|
49
|
+
* rotated token is stranded. The intended consumer is the
|
|
50
|
+
* `axe-auth token` CLI (a one-shot), so this is fine in context;
|
|
51
|
+
* per-request callers should wrap in an in-flight-Promise singleton
|
|
52
|
+
* keyed by issuer.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getValidAccessToken(options: GetValidAccessTokenOptions): Promise<string>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getValidAccessToken = getValidAccessToken;
|
|
4
|
+
const discoverOIDC_1 = require("./discoverOIDC");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const refreshTokens_1 = require("./refreshTokens");
|
|
7
|
+
const tokenStore_1 = require("./tokenStore");
|
|
8
|
+
const DEFAULT_EXPIRY_BUFFER_MS = 60_000;
|
|
9
|
+
function defaultOnWarning(message) {
|
|
10
|
+
if (process.stderr.isTTY) {
|
|
11
|
+
console.error(`axe-auth: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isInvalidGrant(err) {
|
|
15
|
+
return (err instanceof errors_1.OAuthFlowError &&
|
|
16
|
+
err.code === "TOKEN_EXCHANGE_FAILED" &&
|
|
17
|
+
err.details?.error === "invalid_grant");
|
|
18
|
+
}
|
|
19
|
+
function notAuthenticated(message, cause) {
|
|
20
|
+
return new errors_1.OAuthFlowError("NOT_AUTHENTICATED", message, cause === undefined ? undefined : { cause });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns a currently-valid access token string for the given issuer,
|
|
24
|
+
* refreshing via the stored refresh token if the cached access token
|
|
25
|
+
* is within `expiryBufferMs` of expiring (or already expired).
|
|
26
|
+
*
|
|
27
|
+
* Throws `OAuthFlowError("NOT_AUTHENTICATED", ...)` when the user
|
|
28
|
+
* must re-run `axe-auth login` — covers an empty / corrupt /
|
|
29
|
+
* version-mismatched store, an expired access token with no refresh
|
|
30
|
+
* token to rotate with, and a refresh attempt rejected with
|
|
31
|
+
* `invalid_grant` (which also clears the stored tokens).
|
|
32
|
+
*
|
|
33
|
+
* Throws `OAuthFlowError("TOKEN_EXCHANGE_FAILED", ...)` for transient
|
|
34
|
+
* failures during refresh (network errors, 5xx, malformed responses)
|
|
35
|
+
* and leaves the stored tokens intact so a retry is possible.
|
|
36
|
+
*
|
|
37
|
+
* Throws `OAuthFlowError("DISCOVERY_FAILED", ...)` when the issuer
|
|
38
|
+
* URL cannot be reached or parsed at refresh time.
|
|
39
|
+
*
|
|
40
|
+
* **Concurrency note.** Not safe against parallel invocations for
|
|
41
|
+
* the same issuer. Keycloak rotates refresh tokens by default; if
|
|
42
|
+
* two parallel calls both land on the refresh path, only one
|
|
43
|
+
* winner's rotated refresh token will be persisted and the loser's
|
|
44
|
+
* rotated token is stranded. The intended consumer is the
|
|
45
|
+
* `axe-auth token` CLI (a one-shot), so this is fine in context;
|
|
46
|
+
* per-request callers should wrap in an in-flight-Promise singleton
|
|
47
|
+
* keyed by issuer.
|
|
48
|
+
*/
|
|
49
|
+
async function getValidAccessToken(options) {
|
|
50
|
+
const { issuerURL, clientId, expiryBufferMs = DEFAULT_EXPIRY_BUFFER_MS, tokenStore = new tokenStore_1.KeyringTokenStore(), loadedEntry, signal, allowInsecureIssuer, onWarning = defaultOnWarning, now = Date.now, } = options;
|
|
51
|
+
const loaded = loadedEntry ?? (await tokenStore.load());
|
|
52
|
+
if (!loaded.ok) {
|
|
53
|
+
switch (loaded.reason) {
|
|
54
|
+
case "empty":
|
|
55
|
+
throw notAuthenticated("No stored credentials. Run `axe-auth login` first.");
|
|
56
|
+
case "corrupt":
|
|
57
|
+
throw notAuthenticated("Stored credentials are unreadable. Run `axe-auth login` to re-authenticate.");
|
|
58
|
+
case "version-mismatch":
|
|
59
|
+
throw notAuthenticated(`Stored credentials are from an unsupported schema version (v:${loaded.storedVersion}). Run \`axe-auth login\` to re-authenticate.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Refuse on issuer/client mismatch: refreshing tokens at a
|
|
63
|
+
// different endpoint would leak the refresh token to the wrong
|
|
64
|
+
// server.
|
|
65
|
+
if (loaded.entry.issuerURL !== issuerURL ||
|
|
66
|
+
loaded.entry.clientId !== clientId) {
|
|
67
|
+
throw notAuthenticated(`Stored credentials are for issuer ${loaded.entry.issuerURL} (client ${loaded.entry.clientId}), but the requested issuer is ${issuerURL} (client ${clientId}). Run \`axe-auth login\` to re-authenticate.`);
|
|
68
|
+
}
|
|
69
|
+
const tokens = loaded.entry.tokens;
|
|
70
|
+
if (now() + expiryBufferMs < tokens.expiresAt) {
|
|
71
|
+
return tokens.accessToken;
|
|
72
|
+
}
|
|
73
|
+
if (!tokens.refreshToken) {
|
|
74
|
+
throw notAuthenticated("Access token has expired and no refresh token is available. Run `axe-auth login` to re-authenticate.");
|
|
75
|
+
}
|
|
76
|
+
const config = await (0, discoverOIDC_1.discoverOIDC)(issuerURL, {
|
|
77
|
+
signal,
|
|
78
|
+
allowInsecureIssuer,
|
|
79
|
+
});
|
|
80
|
+
let fresh;
|
|
81
|
+
try {
|
|
82
|
+
fresh = await (0, refreshTokens_1.refreshTokens)({
|
|
83
|
+
tokenEndpoint: config.tokenEndpoint,
|
|
84
|
+
clientId,
|
|
85
|
+
refreshToken: tokens.refreshToken,
|
|
86
|
+
now,
|
|
87
|
+
signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (isInvalidGrant(err)) {
|
|
92
|
+
// Best-effort clear: if the clear itself fails, still surface
|
|
93
|
+
// NOT_AUTHENTICATED so the user gets the "please run login"
|
|
94
|
+
// signal — the next run will refresh, land back here, and
|
|
95
|
+
// retry the clear.
|
|
96
|
+
try {
|
|
97
|
+
await tokenStore.clear();
|
|
98
|
+
}
|
|
99
|
+
catch (clearErr) {
|
|
100
|
+
onWarning(`Failed to clear stored tokens after refresh rejection: ${clearErr instanceof Error ? clearErr.message : String(clearErr)}. Next run may need to clear manually.`);
|
|
101
|
+
}
|
|
102
|
+
throw notAuthenticated("Your session has expired. Run `axe-auth login` to re-authenticate.", err);
|
|
103
|
+
}
|
|
104
|
+
// Transient failure — leave the store alone so the user can
|
|
105
|
+
// retry without re-logging-in.
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
// HAZARD: Keycloak (and most rotating-refresh-token providers) have
|
|
109
|
+
// already consumed the old refresh token server-side by the time
|
|
110
|
+
// `refreshTokens` returns. If persisting the rotated set fails
|
|
111
|
+
// here, we hold a valid access token in memory but the stored
|
|
112
|
+
// refresh token is now stale — the next invocation will POST it,
|
|
113
|
+
// get `invalid_grant`, and prompt re-authentication.
|
|
114
|
+
//
|
|
115
|
+
// We still return the fresh access token so the current call is
|
|
116
|
+
// useful, and warn the caller so "why does the next run need me to
|
|
117
|
+
// log in again?" has a breadcrumb.
|
|
118
|
+
try {
|
|
119
|
+
await tokenStore.save({
|
|
120
|
+
tokens: fresh,
|
|
121
|
+
issuerURL: loaded.entry.issuerURL,
|
|
122
|
+
clientId: loaded.entry.clientId,
|
|
123
|
+
allowInsecureIssuer: loaded.entry.allowInsecureIssuer,
|
|
124
|
+
walnutURL: loaded.entry.walnutURL,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
onWarning(`Refreshed tokens could not be saved: ${err instanceof Error ? err.message : String(err)}. The current call will succeed, but the next invocation will require re-authentication.`);
|
|
129
|
+
}
|
|
130
|
+
return fresh.accessToken;
|
|
131
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { startCallbackServer } from "./callbackServer";
|
|
2
|
+
export type { CallbackServerOptions, CallbackServerHandle, CallbackResult, } from "./callbackServer";
|
|
3
|
+
export { OAuthCallbackError, OAuthFlowError } from "./errors";
|
|
4
|
+
export type { OAuthCallbackErrorCode, OAuthCallbackErrorOptions, OAuthFlowErrorCode, OAuthFlowErrorOptions, } from "./errors";
|
|
5
|
+
export { authorize } from "./authorize";
|
|
6
|
+
export type { AuthorizeOptions } from "./authorize";
|
|
7
|
+
export { getValidAccessToken } from "./getValidAccessToken";
|
|
8
|
+
export type { GetValidAccessTokenOptions } from "./getValidAccessToken";
|
|
9
|
+
export { refreshTokens } from "./refreshTokens";
|
|
10
|
+
export type { RefreshTokensOptions } from "./refreshTokens";
|
|
11
|
+
export type { TokenSet } from "./tokenResponse";
|
|
12
|
+
export { KeyringTokenStore, STORED_BLOB_VERSION } from "./tokenStore";
|
|
13
|
+
export type { LoadResult, StoredEntry, TokenStore } from "./tokenStore";
|
|
14
|
+
export type { KeyringEntry, KeyringEntryFactory } from "./keyringBinding";
|
|
15
|
+
export { discoverOIDC } from "./discoverOIDC";
|
|
16
|
+
export type { OIDCConfiguration, DiscoverOIDCOptions } from "./discoverOIDC";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverOIDC = exports.STORED_BLOB_VERSION = exports.KeyringTokenStore = exports.refreshTokens = exports.getValidAccessToken = exports.authorize = exports.OAuthFlowError = exports.OAuthCallbackError = exports.startCallbackServer = void 0;
|
|
4
|
+
var callbackServer_1 = require("./callbackServer");
|
|
5
|
+
Object.defineProperty(exports, "startCallbackServer", { enumerable: true, get: function () { return callbackServer_1.startCallbackServer; } });
|
|
6
|
+
var errors_1 = require("./errors");
|
|
7
|
+
Object.defineProperty(exports, "OAuthCallbackError", { enumerable: true, get: function () { return errors_1.OAuthCallbackError; } });
|
|
8
|
+
Object.defineProperty(exports, "OAuthFlowError", { enumerable: true, get: function () { return errors_1.OAuthFlowError; } });
|
|
9
|
+
var authorize_1 = require("./authorize");
|
|
10
|
+
Object.defineProperty(exports, "authorize", { enumerable: true, get: function () { return authorize_1.authorize; } });
|
|
11
|
+
var getValidAccessToken_1 = require("./getValidAccessToken");
|
|
12
|
+
Object.defineProperty(exports, "getValidAccessToken", { enumerable: true, get: function () { return getValidAccessToken_1.getValidAccessToken; } });
|
|
13
|
+
var refreshTokens_1 = require("./refreshTokens");
|
|
14
|
+
Object.defineProperty(exports, "refreshTokens", { enumerable: true, get: function () { return refreshTokens_1.refreshTokens; } });
|
|
15
|
+
var tokenStore_1 = require("./tokenStore");
|
|
16
|
+
Object.defineProperty(exports, "KeyringTokenStore", { enumerable: true, get: function () { return tokenStore_1.KeyringTokenStore; } });
|
|
17
|
+
Object.defineProperty(exports, "STORED_BLOB_VERSION", { enumerable: true, get: function () { return tokenStore_1.STORED_BLOB_VERSION; } });
|
|
18
|
+
var discoverOIDC_1 = require("./discoverOIDC");
|
|
19
|
+
Object.defineProperty(exports, "discoverOIDC", { enumerable: true, get: function () { return discoverOIDC_1.discoverOIDC; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalizes an OIDC issuer URL for equivalence comparison. Two URLs
|
|
3
|
+
* that normalize to the same string refer to the same issuer, which is
|
|
4
|
+
* what `discoverOIDC` uses to build discovery URLs and what
|
|
5
|
+
* `KeyringTokenStore` uses to key keychain entries.
|
|
6
|
+
*
|
|
7
|
+
* Rules (per RFC 3986 §6.2 URI comparison):
|
|
8
|
+
* - Trailing slashes stripped from the path.
|
|
9
|
+
* - Scheme and authority (host + optional port) lowercased — both are
|
|
10
|
+
* case-insensitive per the RFC.
|
|
11
|
+
* - Default ports (80 for http, 443 for https) collapsed via `URL.host`.
|
|
12
|
+
* - Path preserved case-sensitively.
|
|
13
|
+
* - Query string and fragment dropped: OIDC issuers are defined by
|
|
14
|
+
* scheme + authority + path, and carrying them through would break
|
|
15
|
+
* downstream path concatenation (e.g. appending
|
|
16
|
+
* `/.well-known/openid-configuration`).
|
|
17
|
+
*
|
|
18
|
+
* If the input is not a parseable URL, the function trims trailing
|
|
19
|
+
* slashes and returns — discovery will surface a clearer error than
|
|
20
|
+
* this function could.
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeIssuerURL(url: string): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeIssuerURL = normalizeIssuerURL;
|
|
4
|
+
/**
|
|
5
|
+
* Canonicalizes an OIDC issuer URL for equivalence comparison. Two URLs
|
|
6
|
+
* that normalize to the same string refer to the same issuer, which is
|
|
7
|
+
* what `discoverOIDC` uses to build discovery URLs and what
|
|
8
|
+
* `KeyringTokenStore` uses to key keychain entries.
|
|
9
|
+
*
|
|
10
|
+
* Rules (per RFC 3986 §6.2 URI comparison):
|
|
11
|
+
* - Trailing slashes stripped from the path.
|
|
12
|
+
* - Scheme and authority (host + optional port) lowercased — both are
|
|
13
|
+
* case-insensitive per the RFC.
|
|
14
|
+
* - Default ports (80 for http, 443 for https) collapsed via `URL.host`.
|
|
15
|
+
* - Path preserved case-sensitively.
|
|
16
|
+
* - Query string and fragment dropped: OIDC issuers are defined by
|
|
17
|
+
* scheme + authority + path, and carrying them through would break
|
|
18
|
+
* downstream path concatenation (e.g. appending
|
|
19
|
+
* `/.well-known/openid-configuration`).
|
|
20
|
+
*
|
|
21
|
+
* If the input is not a parseable URL, the function trims trailing
|
|
22
|
+
* slashes and returns — discovery will surface a clearer error than
|
|
23
|
+
* this function could.
|
|
24
|
+
*/
|
|
25
|
+
function normalizeIssuerURL(url) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = new URL(url);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return url.replace(/\/+$/, "");
|
|
32
|
+
}
|
|
33
|
+
// `URL.protocol` is already lowercased by the URL parser.
|
|
34
|
+
// `URL.host` retains input casing, so lowercase it explicitly.
|
|
35
|
+
const host = parsed.host.toLowerCase();
|
|
36
|
+
const pathname = parsed.pathname.replace(/\/+$/, "");
|
|
37
|
+
return `${parsed.protocol}//${host}${pathname}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Minimal keyring-entry surface consumed by the package's stores. */
|
|
2
|
+
export interface KeyringEntry {
|
|
3
|
+
/** Writes the password for this entry. */
|
|
4
|
+
setPassword(password: string): void;
|
|
5
|
+
/** Reads the current password, or returns `null` if none is set. */
|
|
6
|
+
getPassword(): string | null;
|
|
7
|
+
/** Deletes the password and returns `true` if one existed. */
|
|
8
|
+
deletePassword(): boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Factory for `KeyringEntry` values. Injection seam for tests;
|
|
12
|
+
* production callers use the default that constructs
|
|
13
|
+
* `@napi-rs/keyring` entries lazily.
|
|
14
|
+
*/
|
|
15
|
+
export type KeyringEntryFactory = (service: string, account: string) => KeyringEntry;
|
|
16
|
+
/**
|
|
17
|
+
* Default `KeyringEntryFactory`. Resolves `@napi-rs/keyring` lazily
|
|
18
|
+
* so platforms without a prebuilt binding only see a
|
|
19
|
+
* `KEYRING_UNAVAILABLE` on first store construction, not on module
|
|
20
|
+
* import.
|
|
21
|
+
*/
|
|
22
|
+
export declare const defaultEntryFactory: KeyringEntryFactory;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultEntryFactory = void 0;
|
|
4
|
+
const node_module_1 = require("node:module");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const requireFromHere = (0, node_module_1.createRequire)(__filename);
|
|
7
|
+
// Lazy-resolved Entry constructor. Importing @napi-rs/keyring at the
|
|
8
|
+
// top of this module would run its native binding loader at
|
|
9
|
+
// module-load time, throwing before any of our OAuthFlowError code
|
|
10
|
+
// catches it and preventing the module from being imported at all on
|
|
11
|
+
// platforms without a prebuilt. We defer the require into
|
|
12
|
+
// `defaultEntryFactory`, which turns that import-time failure into a
|
|
13
|
+
// `KEYRING_UNAVAILABLE` surfaced on the first store construction —
|
|
14
|
+
// not on first save/load/clear, since callers construct stores
|
|
15
|
+
// eagerly as default-arg expressions. Runtime keychain errors
|
|
16
|
+
// (missing D-Bus Secret Service, macOS Keychain denial, etc.) are a
|
|
17
|
+
// separate concern and surface later, inside save/load/clear.
|
|
18
|
+
let cachedEntryCtor = null;
|
|
19
|
+
function resolveEntryCtor() {
|
|
20
|
+
if (cachedEntryCtor)
|
|
21
|
+
return cachedEntryCtor;
|
|
22
|
+
try {
|
|
23
|
+
const mod = requireFromHere("@napi-rs/keyring");
|
|
24
|
+
cachedEntryCtor = mod.Entry;
|
|
25
|
+
return cachedEntryCtor;
|
|
26
|
+
}
|
|
27
|
+
catch (cause) {
|
|
28
|
+
throw new errors_1.OAuthFlowError("KEYRING_UNAVAILABLE", `Could not load @napi-rs/keyring. A prebuilt native binding for this platform may be missing.`, { cause });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Default `KeyringEntryFactory`. Resolves `@napi-rs/keyring` lazily
|
|
33
|
+
* so platforms without a prebuilt binding only see a
|
|
34
|
+
* `KEYRING_UNAVAILABLE` on first store construction, not on module
|
|
35
|
+
* import.
|
|
36
|
+
*/
|
|
37
|
+
const defaultEntryFactory = (service, account) => {
|
|
38
|
+
const Ctor = resolveEntryCtor();
|
|
39
|
+
return new Ctor(service, account);
|
|
40
|
+
};
|
|
41
|
+
exports.defaultEntryFactory = defaultEntryFactory;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const LOGO_PNG_BASE64 = "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";
|