@deque/axe-auth 1.1.0-next.73fce274 → 1.1.0-next.7469dec9
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 +13 -17
- package/credits.json +53 -0
- package/dist/cli/commonArgs.d.ts +20 -51
- package/dist/cli/commonArgs.help.d.ts +1 -1
- package/dist/cli/commonArgs.help.js +12 -11
- package/dist/cli/commonArgs.js +20 -76
- package/dist/cli/confirm.js +0 -3
- package/dist/cli/errors.d.ts +2 -19
- package/dist/cli/errors.js +3 -25
- package/dist/cli/testUtils.js +3 -3
- package/dist/cli/types.d.ts +10 -53
- package/dist/commands/login.d.ts +4 -4
- package/dist/commands/login.help.d.ts +1 -1
- package/dist/commands/login.help.js +11 -5
- package/dist/commands/login.js +33 -18
- package/dist/commands/logout.d.ts +1 -1
- package/dist/commands/logout.help.d.ts +1 -1
- package/dist/commands/logout.help.js +5 -4
- package/dist/commands/logout.js +1 -17
- package/dist/commands/token.d.ts +2 -7
- package/dist/commands/token.help.d.ts +1 -1
- package/dist/commands/token.help.js +5 -5
- package/dist/commands/token.js +6 -22
- package/dist/index.js +17 -52
- package/dist/oauth/authorizationURL.d.ts +1 -6
- package/dist/oauth/authorizationURL.js +2 -6
- package/dist/oauth/authorize.d.ts +13 -44
- package/dist/oauth/authorize.js +4 -5
- package/dist/oauth/discoverOIDC.d.ts +10 -27
- package/dist/oauth/discoverOIDC.js +33 -32
- package/dist/oauth/discoverSSOConfig.d.ts +37 -0
- package/dist/oauth/discoverSSOConfig.js +105 -0
- package/dist/oauth/errors.d.ts +2 -0
- package/dist/oauth/getValidAccessToken.d.ts +9 -44
- package/dist/oauth/getValidAccessToken.js +8 -16
- package/dist/oauth/openBrowser.d.ts +14 -3
- package/dist/oauth/openBrowser.js +22 -5
- package/dist/oauth/refreshTokens.js +5 -5
- package/dist/oauth/retry.d.ts +1 -0
- package/dist/oauth/retry.js +49 -0
- package/dist/oauth/revokeToken.js +8 -3
- package/dist/oauth/tokenExchange.js +5 -2
- package/dist/oauth/tokenResponse.d.ts +6 -38
- package/dist/oauth/tokenResponse.js +7 -27
- package/dist/oauth/tokenStore.d.ts +63 -3
- package/dist/oauth/tokenStore.js +379 -32
- package/dist/userAgent.d.ts +12 -0
- package/dist/userAgent.js +18 -0
- package/docs/architecture.md +27 -18
- package/package.json +16 -4
|
@@ -6,14 +6,20 @@ const commonArgs_help_1 = require("../cli/commonArgs.help");
|
|
|
6
6
|
exports.HELP_LOGIN = `axe-auth login
|
|
7
7
|
|
|
8
8
|
Open a browser, complete the OAuth 2.0 Authorization Code + PKCE
|
|
9
|
-
flow against
|
|
10
|
-
to the OS keychain.
|
|
9
|
+
flow against the customer's Keycloak realm, and persist the
|
|
10
|
+
resulting tokens to the OS keychain.
|
|
11
|
+
|
|
12
|
+
The CLI discovers the OAuth coordinates by calling
|
|
13
|
+
\`<server>/api/sso-config\` on the axe server, so users only need to
|
|
14
|
+
supply (or default to) the axe server URL — never the underlying
|
|
15
|
+
Keycloak URL, realm, or client ID directly.
|
|
11
16
|
|
|
12
17
|
Usage:
|
|
13
|
-
axe-auth login --server <url>
|
|
18
|
+
axe-auth login [--server <url>] [--force]
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
With no flags, the SaaS prod axe server URL (https://axe.deque.com)
|
|
21
|
+
is used. Customers on other deployments pass --server (or set
|
|
22
|
+
AXE_SERVER_URL).
|
|
17
23
|
|
|
18
24
|
Options:
|
|
19
25
|
${commonArgs_help_1.HELP_COMMON_OPTIONS}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const ts_dedent_1 = require("ts-dedent");
|
|
7
|
+
const remove_trailing_slash_1 = __importDefault(require("remove-trailing-slash"));
|
|
4
8
|
const authorize_1 = require("../oauth/authorize");
|
|
9
|
+
const discoverSSOConfig_1 = require("../oauth/discoverSSOConfig");
|
|
5
10
|
const errors_1 = require("../oauth/errors");
|
|
6
11
|
const tokenStore_1 = require("../oauth/tokenStore");
|
|
7
12
|
const confirm_1 = require("../cli/confirm");
|
|
@@ -19,6 +24,7 @@ const loginCommand = {
|
|
|
19
24
|
async run(args, deps) {
|
|
20
25
|
const isInteractive = deps.isInteractive ?? Boolean(deps.stdin.isTTY);
|
|
21
26
|
const authorizeFn = deps.authorize ?? authorize_1.authorize;
|
|
27
|
+
const discoverFn = deps.discoverSSOConfig ?? discoverSSOConfig_1.discoverSSOConfig;
|
|
22
28
|
const confirmFn = deps.confirm ??
|
|
23
29
|
((prompt) => (0, confirm_1.confirm)({
|
|
24
30
|
question: prompt,
|
|
@@ -26,38 +32,44 @@ const loginCommand = {
|
|
|
26
32
|
output: deps.stderr,
|
|
27
33
|
}));
|
|
28
34
|
const tokenStore = deps.tokenStore ?? new tokenStore_1.KeyringTokenStore();
|
|
35
|
+
let ssoConfig;
|
|
36
|
+
try {
|
|
37
|
+
ssoConfig = await discoverFn(args.walnutURL, {
|
|
38
|
+
allowInsecure: args.allowInsecureIssuer,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (err instanceof errors_1.OAuthFlowError) {
|
|
43
|
+
throw new errors_2.CLIError("OAUTH_FAILED", err.message);
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
const issuerURL = `${(0, remove_trailing_slash_1.default)(ssoConfig.url)}/auth/realms/${ssoConfig.realm}`;
|
|
48
|
+
const clientId = ssoConfig.mcpClientId;
|
|
29
49
|
if (!args.force) {
|
|
30
|
-
// Prefer the entry the dispatcher already loaded; only fall
|
|
31
|
-
// back to a fresh read when there isn't one (test path).
|
|
32
50
|
const stored = deps.loadedEntry ?? (await tokenStore.load());
|
|
33
51
|
if (!stored.ok && stored.reason !== "empty") {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// either way, but the user deserves a breadcrumb for what
|
|
37
|
-
// disappeared.
|
|
52
|
+
// authorize() will overwrite either way, but the user
|
|
53
|
+
// deserves a breadcrumb for what disappeared.
|
|
38
54
|
deps.stderr.write(`axe-auth: replacing unreadable stored credentials (${stored.reason}).\n`);
|
|
39
55
|
}
|
|
40
56
|
if (stored.ok) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Both warrant confirmation; the message text differs so the
|
|
44
|
-
// user understands what's about to be lost.
|
|
45
|
-
const sameIssuer = stored.entry.issuerURL === args.issuerURL &&
|
|
46
|
-
stored.entry.clientId === args.clientId;
|
|
57
|
+
const sameIssuer = stored.entry.issuerURL === issuerURL &&
|
|
58
|
+
stored.entry.clientId === clientId;
|
|
47
59
|
if (!isInteractive) {
|
|
48
60
|
throw new errors_2.CLIError("ALREADY_AUTHENTICATED", sameIssuer
|
|
49
|
-
? `Already authenticated against ${
|
|
61
|
+
? `Already authenticated against ${issuerURL}. Re-run with --force to override.`
|
|
50
62
|
: (0, ts_dedent_1.dedent) `
|
|
51
63
|
Currently authenticated against ${stored.entry.issuerURL}.
|
|
52
|
-
Logging in to ${
|
|
64
|
+
Logging in to ${issuerURL} would replace those tokens.
|
|
53
65
|
Re-run with --force to override.
|
|
54
66
|
`);
|
|
55
67
|
}
|
|
56
68
|
const prompt = sameIssuer
|
|
57
|
-
? `Already authenticated against ${
|
|
69
|
+
? `Already authenticated against ${issuerURL}. Re-authenticate? [y/N]`
|
|
58
70
|
: (0, ts_dedent_1.dedent) `
|
|
59
71
|
Currently authenticated against ${stored.entry.issuerURL} (client ${stored.entry.clientId}).
|
|
60
|
-
Logging in to ${
|
|
72
|
+
Logging in to ${issuerURL} (client ${clientId}) will replace those tokens.
|
|
61
73
|
Continue? [y/N]
|
|
62
74
|
`;
|
|
63
75
|
const ok = await confirmFn(prompt);
|
|
@@ -66,10 +78,13 @@ const loginCommand = {
|
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
}
|
|
81
|
+
// `walnutURL` lands in the StoredEntry so future verbs
|
|
82
|
+
// (re-discovery, revoke) operate without user-supplied flags.
|
|
69
83
|
try {
|
|
70
84
|
await authorizeFn({
|
|
71
|
-
issuerURL
|
|
72
|
-
clientId
|
|
85
|
+
issuerURL,
|
|
86
|
+
clientId,
|
|
87
|
+
walnutURL: args.walnutURL,
|
|
73
88
|
scopes: ["offline_access"],
|
|
74
89
|
allowInsecureIssuer: args.allowInsecureIssuer,
|
|
75
90
|
tokenStore,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Help text for `axe-auth logout --help`. */
|
|
2
|
-
export declare const HELP_LOGOUT = "axe-auth logout\n\nRevoke the stored refresh token server-side (best-effort) and clear\nthe local OS keychain entry.\n\nUsage:\n axe-auth logout\n\n --server / --
|
|
2
|
+
export declare const HELP_LOGOUT = "axe-auth logout\n\nRevoke the stored refresh token server-side (best-effort) and clear\nthe local OS keychain entry.\n\nUsage:\n axe-auth logout\n\n Operates exclusively on the keychain entry written by\n `axe-auth login`. The --server / --allow-insecure-issuer /\n --no-allow-insecure-issuer flags are accepted for parity with\n other commands but ignored; revocation uses the issuer, client,\n and insecure-issuer policy persisted alongside the tokens at login.\n\nOptions:\n --server <url> axe server URL. Used by `login` to fetch\n /api/sso-config and derive the OAuth\n coordinates. Falls back to AXE_SERVER_URL,\n then to https://axe.deque.com (SaaS prod).\n --allow-insecure-issuer Permit non-loopback http URLs (default is\n https only; loopback http is always\n allowed). Applies to `login` only;\n `token` and `logout` use the policy\n persisted at login.\n --no-allow-insecure-issuer\n Force allowInsecureIssuer=false for the new\n `login` (and the entry it persists).\n Ignored by `token` and `logout`.\n Mutually exclusive with\n --allow-insecure-issuer.\n -h, --help Show this help.\n\nBehavior:\n - If no tokens are stored, prints a note and exits 0 (logout is\n idempotent).\n - If the stored blob is unreadable (corrupt or from an\n unsupported schema version), the local entry is cleared without\n attempting server-side revocation, and a warning is printed.\n - Otherwise: discovers the revocation endpoint at the stored\n issuer, POSTs the stored refresh token per RFC 7009, then\n clears the local keychain entry. Revocation failures or a\n missing revocation endpoint are warned about; the local clear\n still runs.\n\nExit codes:\n 0 Tokens cleared (server-side revocation may have failed; see\n stderr for warnings).\n 2 Local clear failure.";
|
|
@@ -11,10 +11,11 @@ the local OS keychain entry.
|
|
|
11
11
|
Usage:
|
|
12
12
|
axe-auth logout
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
Operates exclusively on the keychain entry written by
|
|
15
|
+
\`axe-auth login\`. The --server / --allow-insecure-issuer /
|
|
16
|
+
--no-allow-insecure-issuer flags are accepted for parity with
|
|
17
|
+
other commands but ignored; revocation uses the issuer, client,
|
|
18
|
+
and insecure-issuer policy persisted alongside the tokens at login.
|
|
18
19
|
|
|
19
20
|
Options:
|
|
20
21
|
${commonArgs_help_1.HELP_COMMON_OPTIONS}
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const ts_dedent_1 = require("ts-dedent");
|
|
4
3
|
const discoverOIDC_1 = require("../oauth/discoverOIDC");
|
|
5
4
|
const revokeToken_1 = require("../oauth/revokeToken");
|
|
6
5
|
const tokenStore_1 = require("../oauth/tokenStore");
|
|
@@ -13,12 +12,10 @@ const logoutCommand = {
|
|
|
13
12
|
helpText: logout_help_1.HELP_LOGOUT,
|
|
14
13
|
options: {},
|
|
15
14
|
requiresConfig: false,
|
|
16
|
-
async run(
|
|
15
|
+
async run(_args, deps) {
|
|
17
16
|
const discoverFn = deps.discoverOIDC ?? discoverOIDC_1.discoverOIDC;
|
|
18
17
|
const revokeFn = deps.revokeRefreshToken ?? revokeToken_1.revokeRefreshToken;
|
|
19
18
|
const tokenStore = deps.tokenStore ?? new tokenStore_1.KeyringTokenStore();
|
|
20
|
-
// Prefer the entry the dispatcher already loaded; only fall back
|
|
21
|
-
// to a fresh read when there isn't one (test path).
|
|
22
19
|
const loaded = deps.loadedEntry ?? (await tokenStore.load());
|
|
23
20
|
if (!loaded.ok) {
|
|
24
21
|
if (loaded.reason === "empty") {
|
|
@@ -34,20 +31,7 @@ const logoutCommand = {
|
|
|
34
31
|
deps.stdout.write("✓ Logged out.\n");
|
|
35
32
|
return;
|
|
36
33
|
}
|
|
37
|
-
// Use the stored entry's metadata for revocation, not args. Under
|
|
38
|
-
// single-entry keying there's only one set of credentials in the
|
|
39
|
-
// keychain; trying to revoke A's refresh token at B's revocation
|
|
40
|
-
// endpoint (because the user passed --server B) would just fail
|
|
41
|
-
// and leave A's token valid server-side.
|
|
42
34
|
const { issuerURL, clientId, allowInsecureIssuer } = loaded.entry;
|
|
43
|
-
if (issuerURL !== args.issuerURL ||
|
|
44
|
-
clientId !== args.clientId ||
|
|
45
|
-
allowInsecureIssuer !== args.allowInsecureIssuer) {
|
|
46
|
-
deps.stderr.write((0, ts_dedent_1.dedent) `
|
|
47
|
-
axe-auth: ignoring --server / --realm / --client-id / --allow-insecure-issuer flags.
|
|
48
|
-
Logging out of stored issuer ${issuerURL} (client ${clientId}) instead.
|
|
49
|
-
` + "\n");
|
|
50
|
-
}
|
|
51
35
|
// Best-effort server-side revocation. Any failure here is a
|
|
52
36
|
// warning; the local clear runs unconditionally below so the
|
|
53
37
|
// user is "logged out" from this machine regardless.
|
package/dist/commands/token.d.ts
CHANGED
|
@@ -6,12 +6,7 @@ import type { CommandDeps } from "../cli/types";
|
|
|
6
6
|
export interface TokenDeps extends CommandDeps {
|
|
7
7
|
/** Override `getValidAccessToken` so tests don't touch the keychain or network. */
|
|
8
8
|
getToken?: typeof getValidAccessToken;
|
|
9
|
-
/**
|
|
10
|
-
* Override the token store. Defaults to a fresh
|
|
11
|
-
* `KeyringTokenStore()`. Used to detect when caller-supplied flags
|
|
12
|
-
* mismatch the stored entry's issuer/client so the warning fires
|
|
13
|
-
* with the same store instance the eventual `getToken` call sees.
|
|
14
|
-
*/
|
|
9
|
+
/** Override the token store. Defaults to a fresh `KeyringTokenStore()`. */
|
|
15
10
|
tokenStore?: TokenStore;
|
|
16
11
|
}
|
|
17
12
|
/** `axe-auth token` — print a currently-valid access token to stdout. */
|
|
@@ -21,6 +16,6 @@ declare const tokenCommand: {
|
|
|
21
16
|
helpText: string;
|
|
22
17
|
options: {};
|
|
23
18
|
requiresConfig: false;
|
|
24
|
-
run(
|
|
19
|
+
run(_args: CommonArgs, deps: TokenDeps): Promise<void>;
|
|
25
20
|
};
|
|
26
21
|
export default tokenCommand;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Help text for `axe-auth token --help`. */
|
|
2
|
-
export declare const HELP_TOKEN = "axe-auth token\n\nPrint a currently-valid OAuth access token to stdout, refreshing\nsilently against the stored refresh token if needed.\n\nUsage:\n axe-auth token\n\n --server / --
|
|
2
|
+
export declare const HELP_TOKEN = "axe-auth token\n\nPrint a currently-valid OAuth access token to stdout, refreshing\nsilently against the stored refresh token if needed.\n\nUsage:\n axe-auth token\n\n Operates exclusively on the keychain entry written by\n `axe-auth login`. The --server / --allow-insecure-issuer /\n --no-allow-insecure-issuer flags are accepted for parity with\n other commands but ignored here; refresh uses the issuer, client,\n and insecure-issuer policy persisted alongside the tokens at login.\n\nOptions:\n --server <url> axe server URL. Used by `login` to fetch\n /api/sso-config and derive the OAuth\n coordinates. Falls back to AXE_SERVER_URL,\n then to https://axe.deque.com (SaaS prod).\n --allow-insecure-issuer Permit non-loopback http URLs (default is\n https only; loopback http is always\n allowed). Applies to `login` only;\n `token` and `logout` use the policy\n persisted at login.\n --no-allow-insecure-issuer\n Force allowInsecureIssuer=false for the new\n `login` (and the entry it persists).\n Ignored by `token` and `logout`.\n Mutually exclusive with\n --allow-insecure-issuer.\n -h, --help Show this help.\n\nOutput:\n The access token is written to stdout with a trailing newline so\n shell substitution (`$(axe-auth token)`) works cleanly. Nothing\n else is written to stdout.\n\nSecurity note:\n Using `$(axe-auth token)` in a shell command exposes the access\n token briefly in the system process-list (observable via `ps` on\n POSIX, Task Manager on Windows). OAuth access tokens are\n short-lived (typically minutes), which limits this exposure\n compared to a static API key, but you should prefer streaming the\n token into a file or env var on platforms where process listings\n are sensitive.\n\nExit codes:\n 0 Success; the token was printed.\n 1 Not authenticated; run `axe-auth login` to re-authenticate.\n 2 Configuration error or transient failure (network, server,\n keychain).";
|
|
@@ -11,11 +11,11 @@ silently against the stored refresh token if needed.
|
|
|
11
11
|
Usage:
|
|
12
12
|
axe-auth token
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Operates exclusively on the keychain entry written by
|
|
15
|
+
\`axe-auth login\`. The --server / --allow-insecure-issuer /
|
|
16
|
+
--no-allow-insecure-issuer flags are accepted for parity with
|
|
17
|
+
other commands but ignored here; refresh uses the issuer, client,
|
|
18
|
+
and insecure-issuer policy persisted alongside the tokens at login.
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
21
|
${commonArgs_help_1.HELP_COMMON_OPTIONS}
|
package/dist/commands/token.js
CHANGED
|
@@ -12,34 +12,18 @@ const tokenCommand = {
|
|
|
12
12
|
helpText: token_help_1.HELP_TOKEN,
|
|
13
13
|
options: {},
|
|
14
14
|
requiresConfig: false,
|
|
15
|
-
async run(
|
|
15
|
+
async run(_args, deps) {
|
|
16
16
|
const getToken = deps.getToken ?? getValidAccessToken_1.getValidAccessToken;
|
|
17
17
|
const tokenStore = deps.tokenStore ?? new tokenStore_1.KeyringTokenStore();
|
|
18
|
-
// Prefer the entry the dispatcher already loaded; only fall back
|
|
19
|
-
// to a fresh read when there isn't one (test path).
|
|
20
18
|
const loaded = deps.loadedEntry ?? (await tokenStore.load());
|
|
21
|
-
// The stored entry is the source of truth for issuer/client
|
|
22
|
-
// (single-entry-per-machine model). Refreshing tokens against a
|
|
23
|
-
// different issuer than the one they were minted at would just
|
|
24
|
-
// get them rejected, so args.* are advisory at most: we warn on
|
|
25
|
-
// mismatch and route the refresh to the stored coordinates.
|
|
26
|
-
let target = args;
|
|
27
|
-
if (loaded.ok) {
|
|
28
|
-
if (loaded.entry.issuerURL !== args.issuerURL ||
|
|
29
|
-
loaded.entry.clientId !== args.clientId ||
|
|
30
|
-
loaded.entry.allowInsecureIssuer !== args.allowInsecureIssuer) {
|
|
31
|
-
deps.stderr.write(`axe-auth: ignoring --server / --realm / --client-id / --allow-insecure-issuer flags; using stored issuer ${loaded.entry.issuerURL} (client ${loaded.entry.clientId}).\n`);
|
|
32
|
-
}
|
|
33
|
-
target = {
|
|
34
|
-
issuerURL: loaded.entry.issuerURL,
|
|
35
|
-
clientId: loaded.entry.clientId,
|
|
36
|
-
allowInsecureIssuer: loaded.entry.allowInsecureIssuer,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
19
|
let token;
|
|
40
20
|
try {
|
|
41
21
|
token = await getToken({
|
|
42
|
-
|
|
22
|
+
issuerURL: loaded.ok ? loaded.entry.issuerURL : "",
|
|
23
|
+
clientId: loaded.ok ? loaded.entry.clientId : "",
|
|
24
|
+
allowInsecureIssuer: loaded.ok
|
|
25
|
+
? loaded.entry.allowInsecureIssuer
|
|
26
|
+
: false,
|
|
43
27
|
tokenStore,
|
|
44
28
|
loadedEntry: loaded,
|
|
45
29
|
});
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const node_fs_1 = require("node:fs");
|
|
8
8
|
const node_path_1 = require("node:path");
|
|
9
9
|
const node_util_1 = require("node:util");
|
|
10
|
-
const ts_dedent_1 = require("ts-dedent");
|
|
11
10
|
const commonArgs_1 = require("./cli/commonArgs");
|
|
12
11
|
const tokenStore_1 = require("./oauth/tokenStore");
|
|
13
12
|
const login_1 = __importDefault(require("./commands/login"));
|
|
@@ -15,7 +14,6 @@ const logout_1 = __importDefault(require("./commands/logout"));
|
|
|
15
14
|
const token_1 = __importDefault(require("./commands/token"));
|
|
16
15
|
const errors_1 = require("./cli/errors");
|
|
17
16
|
const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
18
|
-
// Iteration order is the order verbs appear in `axe-auth --help`.
|
|
19
17
|
const COMMANDS = [
|
|
20
18
|
login_1.default,
|
|
21
19
|
logout_1.default,
|
|
@@ -83,64 +81,31 @@ async function dispatch(argv) {
|
|
|
83
81
|
process.stdout.write(`${command.helpText}\n`);
|
|
84
82
|
return 0;
|
|
85
83
|
}
|
|
86
|
-
// Best-effort load
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// is set), so a fully-flagged invocation gains nothing from a
|
|
90
|
-
// keychain hit on the hot path.
|
|
91
|
-
//
|
|
92
|
-
// The load failure isn't fatal on its own — if the user passed
|
|
93
|
-
// flags/env, the stored config doesn't matter — but if
|
|
94
|
-
// `parseCommonArgs` then throws `MissingConfigError`, we surface
|
|
95
|
-
// this failure alongside it so the user understands the keychain
|
|
96
|
-
// (not just missing flags) is the proximate cause.
|
|
84
|
+
// Best-effort load: handed to verbs via `deps.loadedEntry` so a
|
|
85
|
+
// single CLI invocation hits the keychain once. Read failure is
|
|
86
|
+
// non-fatal — verbs handle their own empty/corrupt cases.
|
|
97
87
|
let defaults = null;
|
|
98
|
-
let defaultLoadError = null;
|
|
99
88
|
let loadedEntry;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
issuerURL: loadedEntry.entry.issuerURL,
|
|
108
|
-
clientId: loadedEntry.entry.clientId,
|
|
109
|
-
allowInsecureIssuer: loadedEntry.entry.allowInsecureIssuer,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch (err) {
|
|
114
|
-
defaultLoadError = err;
|
|
89
|
+
try {
|
|
90
|
+
loadedEntry = await new tokenStore_1.KeyringTokenStore().load();
|
|
91
|
+
if (loadedEntry.ok) {
|
|
92
|
+
defaults = {
|
|
93
|
+
walnutURL: loadedEntry.entry.walnutURL,
|
|
94
|
+
allowInsecureIssuer: loadedEntry.entry.allowInsecureIssuer,
|
|
95
|
+
};
|
|
115
96
|
}
|
|
116
97
|
}
|
|
98
|
+
catch {
|
|
99
|
+
// Keychain unavailable: leave defaults null. Verbs surface their
|
|
100
|
+
// own clearer errors at the actual save / read site.
|
|
101
|
+
}
|
|
117
102
|
let common;
|
|
118
103
|
try {
|
|
119
|
-
common = (0, commonArgs_1.parseCommonArgs)(
|
|
104
|
+
common = (0, commonArgs_1.parseCommonArgs)(parsed.values, process.env, defaults);
|
|
120
105
|
}
|
|
121
106
|
catch (err) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// logout). It has its own handling for the empty / corrupt /
|
|
125
|
-
// version-mismatch cases, which is friendlier than the
|
|
126
|
-
// generic "missing required configuration" error
|
|
127
|
-
// (`No stored credentials. Run \`axe-auth login\` first.` for
|
|
128
|
-
// token; `Already logged out.` for logout). Pass a
|
|
129
|
-
// sentinel-empty CommonArgs so the verb runs and decides.
|
|
130
|
-
common = { issuerURL: "", clientId: "", allowInsecureIssuer: false };
|
|
131
|
-
}
|
|
132
|
-
else if (err instanceof errors_1.MissingConfigError && defaultLoadError !== null) {
|
|
133
|
-
process.stderr.write((0, ts_dedent_1.dedent) `
|
|
134
|
-
${err.message}
|
|
135
|
-
Could not read the stored credentials from the keychain (${(0, errors_1.describeError)(defaultLoadError)});
|
|
136
|
-
pass the flags explicitly or fix the keychain.
|
|
137
|
-
` + "\n");
|
|
138
|
-
return 2;
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
process.stderr.write(`${(0, errors_1.describeError)(err)}\n`);
|
|
142
|
-
return 2;
|
|
143
|
-
}
|
|
107
|
+
process.stderr.write(`${(0, errors_1.describeError)(err)}\n`);
|
|
108
|
+
return 2;
|
|
144
109
|
}
|
|
145
110
|
const deps = {
|
|
146
111
|
stdin: process.stdin,
|
|
@@ -10,12 +10,7 @@ export interface BuildAuthorizationURLOptions {
|
|
|
10
10
|
codeChallenge: string;
|
|
11
11
|
/** CSRF `state` value, echoed by the auth server and validated on callback. */
|
|
12
12
|
state: string;
|
|
13
|
-
/**
|
|
14
|
-
* OAuth scopes to request. No default — callers must choose explicitly.
|
|
15
|
-
* Keycloak-idiomatic value for a refresh-token flow is `["offline_access"]`;
|
|
16
|
-
* Google uses `access_type=offline` (a custom parameter) instead; Auth0
|
|
17
|
-
* tolerates `offline_access` only with specific audience settings.
|
|
18
|
-
*/
|
|
13
|
+
/** OAuth scopes to request. No default; callers choose explicitly. */
|
|
19
14
|
scopes: readonly string[];
|
|
20
15
|
}
|
|
21
16
|
/**
|
|
@@ -2,12 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildAuthorizationURL = buildAuthorizationURL;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// something is wrong on the server side (or with the caller's
|
|
8
|
-
// endpoint override) and silently keeping both values would be a
|
|
9
|
-
// security trap: the authorization server's disambiguation is
|
|
10
|
-
// unspecified and varies by implementation.
|
|
5
|
+
// Pre-existing values for these on the authorization endpoint are a
|
|
6
|
+
// security trap: the auth server's disambiguation is unspecified.
|
|
11
7
|
const OAUTH_REQUIRED_PARAMS = [
|
|
12
8
|
"response_type",
|
|
13
9
|
"client_id",
|
|
@@ -2,65 +2,34 @@ import type { TokenSet } from "./tokenResponse";
|
|
|
2
2
|
import { type TokenStore } from "./tokenStore";
|
|
3
3
|
/** Options for `authorize`. */
|
|
4
4
|
export interface AuthorizeOptions {
|
|
5
|
-
/**
|
|
6
|
-
* Authorization-server URL the discovery document claims as its
|
|
7
|
-
* `issuer`. For Keycloak, callers build this as
|
|
8
|
-
* `${serverURL}/realms/${realm}`. For other providers it is the
|
|
9
|
-
* hostname (or issuer path) advertised in their discovery document.
|
|
10
|
-
*/
|
|
5
|
+
/** Issuer URL the OIDC discovery document advertises (e.g. `${serverURL}/realms/${realm}` for Keycloak). */
|
|
11
6
|
issuerURL: string;
|
|
12
7
|
/** OAuth client ID registered with the authorization server. */
|
|
13
8
|
clientId: string;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* want a refresh token typically pass `["offline_access"]`; Google
|
|
18
|
-
* uses `access_type=offline` as a separate query param and
|
|
19
|
-
* therefore needs an empty scope list plus that param threaded
|
|
20
|
-
* through elsewhere.
|
|
21
|
-
*/
|
|
9
|
+
/** Persisted alongside the tokens so future verbs can re-discover `/api/sso-config` without flags. */
|
|
10
|
+
walnutURL: string;
|
|
11
|
+
/** OAuth scopes to request. Keycloak callers typically pass `["offline_access"]` for a refresh token. */
|
|
22
12
|
scopes: readonly string[];
|
|
23
13
|
/** Max time to wait for the loopback callback, in milliseconds. */
|
|
24
14
|
timeoutMs?: number;
|
|
25
15
|
/** Aborts the in-flight discovery, callback wait, and token exchange. */
|
|
26
16
|
signal?: AbortSignal;
|
|
27
|
-
/**
|
|
28
|
-
* Override for the token persistence layer. Defaults to a fresh
|
|
29
|
-
* `KeyringTokenStore()` (single keychain entry per machine; the
|
|
30
|
-
* blob carries its own issuer/client coordinates).
|
|
31
|
-
*/
|
|
17
|
+
/** Override for the token persistence layer. */
|
|
32
18
|
tokenStore?: TokenStore;
|
|
33
19
|
/** Override for the system browser launcher. Injected for tests. */
|
|
34
20
|
openBrowser?: (url: string) => void;
|
|
35
|
-
/**
|
|
36
|
-
* Called with the authorization URL just before the browser launch.
|
|
37
|
-
* The default prints to stderr only when stderr is a TTY, so a
|
|
38
|
-
* parent CLI consuming this library as a dependency does not
|
|
39
|
-
* double-print. Pass a custom handler to route the URL through your
|
|
40
|
-
* own UI, or `() => {}` to suppress entirely.
|
|
41
|
-
*/
|
|
21
|
+
/** Called with the authorization URL just before the browser launch. Default prints to stderr only when stderr is a TTY. */
|
|
42
22
|
onAuthorizationUrl?: (url: string) => void;
|
|
43
23
|
/**
|
|
44
|
-
* Called for soft warnings
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* Non-TTY callers who want warning visibility (log files, parent
|
|
52
|
-
* CLIs, background workers) should pass an explicit handler.
|
|
53
|
-
* Dropped warnings have no visible symptom at the time they fire —
|
|
54
|
-
* users only discover the consequence later (e.g. being prompted to
|
|
55
|
-
* re-authenticate at the next session).
|
|
24
|
+
* Called for soft warnings (e.g. requested `offline_access` but the
|
|
25
|
+
* server returned no refresh token, or the browser failed to
|
|
26
|
+
* launch). Default prints to stderr only when stderr is a TTY.
|
|
27
|
+
* Non-TTY callers who want warning visibility should pass an
|
|
28
|
+
* explicit handler — dropped warnings have no symptom at the time
|
|
29
|
+
* they fire; users discover the consequence later.
|
|
56
30
|
*/
|
|
57
31
|
onWarning?: (message: string) => void;
|
|
58
|
-
/**
|
|
59
|
-
* Forwarded to the discovery step. Loopback hosts (`localhost` /
|
|
60
|
-
* `127.0.0.1` / `[::1]`) are always permitted over http; this flag
|
|
61
|
-
* is the opt-in for non-loopback http issuers and for non-loopback
|
|
62
|
-
* http endpoints returned by discovery. Default `false`.
|
|
63
|
-
*/
|
|
32
|
+
/** Forwarded to discovery; permits non-loopback http issuers + endpoints. */
|
|
64
33
|
allowInsecureIssuer?: boolean;
|
|
65
34
|
}
|
|
66
35
|
/**
|
package/dist/oauth/authorize.js
CHANGED
|
@@ -38,11 +38,9 @@ function defaultOnWarning(message) {
|
|
|
38
38
|
* @throws {OAuthCallbackError} For loopback/callback-server failures.
|
|
39
39
|
*/
|
|
40
40
|
async function authorize(options) {
|
|
41
|
-
const { issuerURL, clientId, scopes, timeoutMs, signal, tokenStore = new tokenStore_1.KeyringTokenStore(), openBrowser = openBrowser_1.openBrowser, onAuthorizationUrl = defaultOnAuthorizationUrl, onWarning = defaultOnWarning, allowInsecureIssuer, } = options;
|
|
42
|
-
// Discovery
|
|
43
|
-
//
|
|
44
|
-
// strictly more useful than a browser tab pointing at a
|
|
45
|
-
// wrong/unreachable URL.
|
|
41
|
+
const { issuerURL, clientId, walnutURL, scopes, timeoutMs, signal, tokenStore = new tokenStore_1.KeyringTokenStore(), openBrowser = openBrowser_1.openBrowser, onAuthorizationUrl = defaultOnAuthorizationUrl, onWarning = defaultOnWarning, allowInsecureIssuer, } = options;
|
|
42
|
+
// Discovery before browser-launch so a bad URL surfaces as a
|
|
43
|
+
// throw rather than a wrong/unreachable browser tab.
|
|
46
44
|
const config = await (0, discoverOIDC_1.discoverOIDC)(issuerURL, {
|
|
47
45
|
signal,
|
|
48
46
|
allowInsecureIssuer,
|
|
@@ -109,6 +107,7 @@ async function authorize(options) {
|
|
|
109
107
|
issuerURL,
|
|
110
108
|
clientId,
|
|
111
109
|
allowInsecureIssuer: allowInsecureIssuer ?? false,
|
|
110
|
+
walnutURL,
|
|
112
111
|
});
|
|
113
112
|
return tokens;
|
|
114
113
|
}
|
|
@@ -15,36 +15,19 @@ export interface OIDCConfiguration {
|
|
|
15
15
|
export interface DiscoverOIDCOptions {
|
|
16
16
|
/** Aborts the underlying fetch when fired. */
|
|
17
17
|
signal?: AbortSignal;
|
|
18
|
-
/**
|
|
19
|
-
* Permit non-HTTPS issuer URLs whose host is not a loopback literal.
|
|
20
|
-
* Loopback hosts (`localhost`, `127.0.0.1`, `[::1]`) are always
|
|
21
|
-
* allowed over http since they cannot be intercepted remotely; this
|
|
22
|
-
* flag is for corporate dev setups or reverse-proxy scenarios where
|
|
23
|
-
* http is the only available path. Default `false`.
|
|
24
|
-
*/
|
|
18
|
+
/** Permit non-HTTPS issuer URLs whose host is not a loopback literal. Default `false`. */
|
|
25
19
|
allowInsecureIssuer?: boolean;
|
|
26
20
|
}
|
|
27
21
|
/**
|
|
28
|
-
* Fetches and parses the
|
|
29
|
-
*
|
|
30
|
-
*
|
|
22
|
+
* Fetches and parses the OIDC discovery document. Fails fast (no
|
|
23
|
+
* retry) so the caller does not open a browser against an unreachable
|
|
24
|
+
* authorization server. Verifies the server's claimed `issuer` matches
|
|
25
|
+
* the input URL per OIDC Discovery §3 — without this, a hostile
|
|
26
|
+
* discovery response could redirect the authorization and token
|
|
27
|
+
* endpoints to attacker hosts.
|
|
31
28
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* itself does not perform OIDC identity validation (no id_token /
|
|
36
|
-
* nonce / signature checks); callers needing OIDC-strength identity
|
|
37
|
-
* assurance should layer that on top.
|
|
38
|
-
*
|
|
39
|
-
* Verifies that the server's claimed `issuer` matches the URL the
|
|
40
|
-
* caller passed in, per OIDC Discovery §3 / defence against a hostile
|
|
41
|
-
* discovery response redirecting `authorization_endpoint` and
|
|
42
|
-
* `token_endpoint` to attacker-controlled hosts.
|
|
43
|
-
*
|
|
44
|
-
* @param issuerURL Authorization-server URL the discovery document
|
|
45
|
-
* claims as its `issuer`. For Keycloak, callers build this as
|
|
46
|
-
* `${serverURL}/realms/${realm}`. For other providers it is the
|
|
47
|
-
* hostname (or issuer path) advertised in their discovery document.
|
|
48
|
-
* Trailing slashes tolerated.
|
|
29
|
+
* Uses the OIDC well-known path as a convention; does not perform
|
|
30
|
+
* OIDC-strength identity validation (no id_token / nonce / signature
|
|
31
|
+
* checks). Callers needing identity assurance should layer that on top.
|
|
49
32
|
*/
|
|
50
33
|
export declare function discoverOIDC(issuerURL: string, options?: DiscoverOIDCOptions): Promise<OIDCConfiguration>;
|