@deque/axe-auth 1.1.0-next.97bcb8e6 → 1.1.0-next.d59ba863
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 +42 -0
- package/dist/cli/commonArgs.d.ts +82 -0
- package/dist/cli/commonArgs.help.d.ts +2 -0
- package/dist/cli/commonArgs.help.js +20 -0
- package/dist/cli/commonArgs.js +90 -0
- package/dist/cli/confirm.d.ts +17 -0
- package/dist/cli/confirm.js +56 -0
- package/dist/cli/errors.d.ts +20 -0
- package/dist/cli/errors.js +37 -0
- package/dist/cli/testUtils.d.ts +52 -0
- package/dist/cli/testUtils.js +100 -0
- package/dist/cli/types.d.ts +79 -0
- package/dist/cli/types.js +2 -0
- package/dist/commands/login.d.ts +44 -0
- package/dist/commands/login.help.d.ts +2 -0
- package/dist/commands/login.help.js +41 -0
- package/dist/commands/login.js +117 -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 +70 -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 +44 -0
- package/dist/index.js +126 -27
- package/dist/oauth/authorizationURL.d.ts +29 -0
- package/dist/oauth/authorizationURL.js +52 -0
- package/dist/oauth/authorize.d.ts +91 -0
- package/dist/oauth/authorize.js +119 -0
- package/dist/oauth/callbackServer.d.ts +23 -0
- package/dist/oauth/callbackServer.js +234 -0
- package/dist/oauth/discoverOIDC.d.ts +50 -0
- package/dist/oauth/discoverOIDC.js +173 -0
- package/dist/oauth/discoverSSOConfig.d.ts +47 -0
- package/dist/oauth/discoverSSOConfig.js +105 -0
- package/dist/oauth/errors.d.ts +75 -0
- package/dist/oauth/errors.js +48 -0
- package/dist/oauth/getValidAccessToken.d.ts +89 -0
- package/dist/oauth/getValidAccessToken.js +140 -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 +19 -0
- package/dist/oauth/openBrowser.js +78 -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 +63 -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 +54 -0
- package/dist/oauth/tokenResponse.js +121 -0
- package/dist/oauth/tokenStore.d.ts +116 -0
- package/dist/oauth/tokenStore.js +202 -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 +16 -3
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { ParseArgsConfig } from "node:util";
|
|
3
|
+
import type { LoadResult } from "../oauth/tokenStore";
|
|
4
|
+
import type { CommonArgs } from "./commonArgs";
|
|
5
|
+
/** Mapping passed to `parseArgs` for verb-specific flags. */
|
|
6
|
+
export type VerbOptions = NonNullable<ParseArgsConfig["options"]>;
|
|
7
|
+
/**
|
|
8
|
+
* The injectables every CLI verb sees. The dispatcher fills these in
|
|
9
|
+
* with the real `process.*` streams; tests pass synthetic values.
|
|
10
|
+
* Verb-specific overrides (e.g. `getToken`, `authorize`) extend this
|
|
11
|
+
* interface in the relevant verb's module — TS allows the dispatcher
|
|
12
|
+
* to pass a plain `CommandDeps` because the verb-specific extras are
|
|
13
|
+
* declared optional.
|
|
14
|
+
*/
|
|
15
|
+
export interface CommandDeps {
|
|
16
|
+
/**
|
|
17
|
+
* Standard input. Carries an optional `isTTY` so verbs like
|
|
18
|
+
* `login` can branch on interactivity. Real `process.stdin`
|
|
19
|
+
* satisfies the shape; test fixtures using `Readable.from([])`
|
|
20
|
+
* leave `isTTY` undefined, which coerces to `false`.
|
|
21
|
+
*/
|
|
22
|
+
stdin: Readable & {
|
|
23
|
+
isTTY?: boolean;
|
|
24
|
+
};
|
|
25
|
+
stdout: Writable;
|
|
26
|
+
stderr: Writable;
|
|
27
|
+
/**
|
|
28
|
+
* Pre-loaded result of `KeyringTokenStore.load()`, populated by the
|
|
29
|
+
* dispatcher when it ran the keychain read to derive the
|
|
30
|
+
* `parseCommonArgs` fallback. Verbs prefer this over loading
|
|
31
|
+
* again so a single `axe-auth token` invocation hits the keychain
|
|
32
|
+
* once instead of three times. Tests typically leave this
|
|
33
|
+
* undefined and inject a fake `tokenStore` instead.
|
|
34
|
+
*/
|
|
35
|
+
loadedEntry?: LoadResult;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Specification of a single CLI verb. The dispatcher in
|
|
39
|
+
* `src/index.ts` consumes one of these per registered command:
|
|
40
|
+
* parses argv, resolves common args, prints `helpText` for `--help`,
|
|
41
|
+
* runs `run`, and translates thrown `CLIError`s (see `./errors`)
|
|
42
|
+
* into exit codes.
|
|
43
|
+
*
|
|
44
|
+
* Each verb narrows the `run` parameters to its own
|
|
45
|
+
* `CommonArgs & <Verb>Flags` and `<Verb>Deps`. Method-shorthand
|
|
46
|
+
* bivariance lets that narrower signature satisfy this interface
|
|
47
|
+
* without casts at the verb definition.
|
|
48
|
+
*/
|
|
49
|
+
export interface CommandSpec {
|
|
50
|
+
/** Verb name, e.g. `"login"`. */
|
|
51
|
+
readonly name: string;
|
|
52
|
+
/** One-liner shown in the top-level `axe-auth --help` listing. */
|
|
53
|
+
readonly summary: string;
|
|
54
|
+
/** Full help text printed for `axe-auth <verb> --help`. */
|
|
55
|
+
readonly helpText: string;
|
|
56
|
+
/**
|
|
57
|
+
* Verb-specific `parseArgs` options. Common options
|
|
58
|
+
* (`--server` / `--allow-insecure-issuer` / `--no-allow-insecure-issuer`)
|
|
59
|
+
* are added by the dispatcher; do not duplicate them here.
|
|
60
|
+
*/
|
|
61
|
+
readonly options: VerbOptions;
|
|
62
|
+
/**
|
|
63
|
+
* `true` if this verb cannot run without a usable walnut URL
|
|
64
|
+
* (login). `false` for verbs that operate on the stored entry
|
|
65
|
+
* alone (token, logout) and have their own "not authenticated" /
|
|
66
|
+
* "already logged out" handling for the empty-entry case. With
|
|
67
|
+
* the SaaS prod default in `parseCommonArgs`, the walnut URL is
|
|
68
|
+
* never strictly missing, but this flag remains for future
|
|
69
|
+
* required-config additions.
|
|
70
|
+
*/
|
|
71
|
+
readonly requiresConfig: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Run the verb with already-resolved args and the dispatcher's
|
|
74
|
+
* deps. Throw a `CLIError` to signal a known failure with an
|
|
75
|
+
* explicit exit code; throw any other error for a generic exit
|
|
76
|
+
* code 2 plus the error message on stderr.
|
|
77
|
+
*/
|
|
78
|
+
run(args: CommonArgs, deps: CommandDeps): Promise<void>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { authorize } from "../oauth/authorize";
|
|
2
|
+
import { discoverSSOConfig } from "../oauth/discoverSSOConfig";
|
|
3
|
+
import { type TokenStore } from "../oauth/tokenStore";
|
|
4
|
+
import type { CommonArgs } from "../cli/commonArgs";
|
|
5
|
+
import type { CommandDeps } from "../cli/types";
|
|
6
|
+
/** Verb-specific deps for `axe-auth login`. */
|
|
7
|
+
export interface LoginDeps extends CommandDeps {
|
|
8
|
+
/** Whether to treat the session as interactive. Defaults to `stdin.isTTY`. */
|
|
9
|
+
isInteractive?: boolean;
|
|
10
|
+
/** Override `authorize` (for tests). */
|
|
11
|
+
authorize?: typeof authorize;
|
|
12
|
+
/** Override the SSO discovery helper (for tests). */
|
|
13
|
+
discoverSSOConfig?: typeof discoverSSOConfig;
|
|
14
|
+
/**
|
|
15
|
+
* Override the token store. Defaults to a fresh
|
|
16
|
+
* `KeyringTokenStore()`. The same instance is passed to `authorize`
|
|
17
|
+
* so the pre-check and post-flow save agree on the keychain entry.
|
|
18
|
+
*/
|
|
19
|
+
tokenStore?: TokenStore;
|
|
20
|
+
/**
|
|
21
|
+
* Override the confirmation prompt (for tests). Receives the
|
|
22
|
+
* issuer URL and returns whether the user wants to proceed.
|
|
23
|
+
*/
|
|
24
|
+
confirm?: (prompt: string) => Promise<boolean>;
|
|
25
|
+
}
|
|
26
|
+
/** Verb-specific flags for `axe-auth login`. */
|
|
27
|
+
export interface LoginFlags {
|
|
28
|
+
/** Set by `--force`. Skip the "already authenticated" prompt. */
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}
|
|
31
|
+
/** `axe-auth login` — drive the OAuth flow and persist tokens. */
|
|
32
|
+
declare const loginCommand: {
|
|
33
|
+
name: string;
|
|
34
|
+
summary: string;
|
|
35
|
+
helpText: string;
|
|
36
|
+
options: {
|
|
37
|
+
force: {
|
|
38
|
+
type: "boolean";
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
requiresConfig: true;
|
|
42
|
+
run(args: CommonArgs & LoginFlags, deps: LoginDeps): Promise<void>;
|
|
43
|
+
};
|
|
44
|
+
export default loginCommand;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Help text for `axe-auth login --help`. */
|
|
2
|
+
export declare const HELP_LOGIN = "axe-auth login\n\nOpen a browser, complete the OAuth 2.0 Authorization Code + PKCE\nflow against the customer's Keycloak realm, and persist the\nresulting tokens to the OS keychain.\n\nThe CLI discovers the OAuth coordinates by calling\n`<server>/api/sso-config` on the axe server, so users only need to\nsupply (or default to) the axe server URL \u2014 never the underlying\nKeycloak URL, realm, or client ID directly.\n\nUsage:\n axe-auth login [--server <url>] [--force]\n\n With no flags, the SaaS prod axe server URL (https://axe.deque.com)\n is used. Customers on other deployments pass --server (or set\n AXE_SERVER_URL).\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 --force Re-authenticate without prompting even if\n a valid token is already stored.\n\nBehavior when already authenticated:\n axe-auth stores one entry per machine. If a valid entry already\n exists, an interactive session prompts for confirmation before\n overwriting it \u2014 even when the new login targets a different\n issuer or client (logging in to B destroys A's tokens). Pass\n --force to skip the prompt. In a non-interactive session (no TTY)\n --force is required; otherwise the command refuses to overwrite\n stored tokens and exits non-zero.\n\nExit codes:\n 0 Success; tokens persisted to the keychain.\n 2 Configuration error or flow failure.\n 3 Login cancelled at the prompt.";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP_LOGIN = void 0;
|
|
4
|
+
const commonArgs_help_1 = require("../cli/commonArgs.help");
|
|
5
|
+
/** Help text for `axe-auth login --help`. */
|
|
6
|
+
exports.HELP_LOGIN = `axe-auth login
|
|
7
|
+
|
|
8
|
+
Open a browser, complete the OAuth 2.0 Authorization Code + PKCE
|
|
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.
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
axe-auth login [--server <url>] [--force]
|
|
19
|
+
|
|
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).
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
${commonArgs_help_1.HELP_COMMON_OPTIONS}
|
|
26
|
+
--force Re-authenticate without prompting even if
|
|
27
|
+
a valid token is already stored.
|
|
28
|
+
|
|
29
|
+
Behavior when already authenticated:
|
|
30
|
+
axe-auth stores one entry per machine. If a valid entry already
|
|
31
|
+
exists, an interactive session prompts for confirmation before
|
|
32
|
+
overwriting it — even when the new login targets a different
|
|
33
|
+
issuer or client (logging in to B destroys A's tokens). Pass
|
|
34
|
+
--force to skip the prompt. In a non-interactive session (no TTY)
|
|
35
|
+
--force is required; otherwise the command refuses to overwrite
|
|
36
|
+
stored tokens and exits non-zero.
|
|
37
|
+
|
|
38
|
+
Exit codes:
|
|
39
|
+
0 Success; tokens persisted to the keychain.
|
|
40
|
+
2 Configuration error or flow failure.
|
|
41
|
+
3 Login cancelled at the prompt.`;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const ts_dedent_1 = require("ts-dedent");
|
|
7
|
+
const remove_trailing_slash_1 = __importDefault(require("remove-trailing-slash"));
|
|
8
|
+
const authorize_1 = require("../oauth/authorize");
|
|
9
|
+
const discoverSSOConfig_1 = require("../oauth/discoverSSOConfig");
|
|
10
|
+
const errors_1 = require("../oauth/errors");
|
|
11
|
+
const tokenStore_1 = require("../oauth/tokenStore");
|
|
12
|
+
const confirm_1 = require("../cli/confirm");
|
|
13
|
+
const login_help_1 = require("./login.help");
|
|
14
|
+
const errors_2 = require("../cli/errors");
|
|
15
|
+
/** `axe-auth login` — drive the OAuth flow and persist tokens. */
|
|
16
|
+
const loginCommand = {
|
|
17
|
+
name: "login",
|
|
18
|
+
summary: "Open a browser, complete the OAuth flow, and persist tokens to the OS keychain.",
|
|
19
|
+
helpText: login_help_1.HELP_LOGIN,
|
|
20
|
+
options: {
|
|
21
|
+
force: { type: "boolean" },
|
|
22
|
+
},
|
|
23
|
+
requiresConfig: true,
|
|
24
|
+
async run(args, deps) {
|
|
25
|
+
const isInteractive = deps.isInteractive ?? Boolean(deps.stdin.isTTY);
|
|
26
|
+
const authorizeFn = deps.authorize ?? authorize_1.authorize;
|
|
27
|
+
const discoverFn = deps.discoverSSOConfig ?? discoverSSOConfig_1.discoverSSOConfig;
|
|
28
|
+
const confirmFn = deps.confirm ??
|
|
29
|
+
((prompt) => (0, confirm_1.confirm)({
|
|
30
|
+
question: prompt,
|
|
31
|
+
input: deps.stdin,
|
|
32
|
+
output: deps.stderr,
|
|
33
|
+
}));
|
|
34
|
+
const tokenStore = deps.tokenStore ?? new tokenStore_1.KeyringTokenStore();
|
|
35
|
+
// 1. Ask the axe server where its Keycloak lives and which client to use.
|
|
36
|
+
// This is the only step that hits the axe server directly; from here on
|
|
37
|
+
// the CLI talks to Keycloak using the discovered coordinates.
|
|
38
|
+
let ssoConfig;
|
|
39
|
+
try {
|
|
40
|
+
ssoConfig = await discoverFn(args.walnutURL, {
|
|
41
|
+
allowInsecure: args.allowInsecureIssuer,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err instanceof errors_1.OAuthFlowError) {
|
|
46
|
+
throw new errors_2.CLIError("OAUTH_FAILED", err.message);
|
|
47
|
+
}
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
const issuerURL = `${(0, remove_trailing_slash_1.default)(ssoConfig.url)}/auth/realms/${ssoConfig.realm}`;
|
|
51
|
+
const clientId = ssoConfig.mcpClientId;
|
|
52
|
+
// 2. Re-auth confirmation. Same UX as the previous flag-driven
|
|
53
|
+
// flow, but the comparison is against the *discovered* issuer
|
|
54
|
+
// + client, not user-supplied flags.
|
|
55
|
+
if (!args.force) {
|
|
56
|
+
const stored = deps.loadedEntry ?? (await tokenStore.load());
|
|
57
|
+
if (!stored.ok && stored.reason !== "empty") {
|
|
58
|
+
// Existing entry is unreadable (corrupt or stored under a
|
|
59
|
+
// schema we can't migrate). authorize() will overwrite it
|
|
60
|
+
// either way, but the user deserves a breadcrumb for what
|
|
61
|
+
// disappeared.
|
|
62
|
+
deps.stderr.write(`axe-auth: replacing unreadable stored credentials (${stored.reason}).\n`);
|
|
63
|
+
}
|
|
64
|
+
if (stored.ok) {
|
|
65
|
+
const sameIssuer = stored.entry.issuerURL === issuerURL &&
|
|
66
|
+
stored.entry.clientId === clientId;
|
|
67
|
+
if (!isInteractive) {
|
|
68
|
+
throw new errors_2.CLIError("ALREADY_AUTHENTICATED", sameIssuer
|
|
69
|
+
? `Already authenticated against ${issuerURL}. Re-run with --force to override.`
|
|
70
|
+
: (0, ts_dedent_1.dedent) `
|
|
71
|
+
Currently authenticated against ${stored.entry.issuerURL}.
|
|
72
|
+
Logging in to ${issuerURL} would replace those tokens.
|
|
73
|
+
Re-run with --force to override.
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
const prompt = sameIssuer
|
|
77
|
+
? `Already authenticated against ${issuerURL}. Re-authenticate? [y/N]`
|
|
78
|
+
: (0, ts_dedent_1.dedent) `
|
|
79
|
+
Currently authenticated against ${stored.entry.issuerURL} (client ${stored.entry.clientId}).
|
|
80
|
+
Logging in to ${issuerURL} (client ${clientId}) will replace those tokens.
|
|
81
|
+
Continue? [y/N]
|
|
82
|
+
`;
|
|
83
|
+
const ok = await confirmFn(prompt);
|
|
84
|
+
if (!ok) {
|
|
85
|
+
throw new errors_2.CLIError("USER_CANCELLED", "Login cancelled.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 3. Drive the OAuth flow. The originating axe server URL rides along
|
|
90
|
+
// so it lands in the StoredEntry and future verbs (re-discovery,
|
|
91
|
+
// revoke) can operate without user-supplied flags.
|
|
92
|
+
try {
|
|
93
|
+
await authorizeFn({
|
|
94
|
+
issuerURL,
|
|
95
|
+
clientId,
|
|
96
|
+
walnutURL: args.walnutURL,
|
|
97
|
+
scopes: ["offline_access"],
|
|
98
|
+
allowInsecureIssuer: args.allowInsecureIssuer,
|
|
99
|
+
tokenStore,
|
|
100
|
+
onAuthorizationUrl: (url) => {
|
|
101
|
+
deps.stderr.write(`Authorization URL: ${url}\n`);
|
|
102
|
+
},
|
|
103
|
+
onWarning: (msg) => {
|
|
104
|
+
deps.stderr.write(`axe-auth: ${msg}\n`);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
if (err instanceof errors_1.OAuthFlowError || err instanceof errors_1.OAuthCallbackError) {
|
|
110
|
+
throw new errors_2.CLIError("OAUTH_FAILED", err.message);
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
deps.stdout.write("✓ Authenticated.\n");
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
exports.default = loginCommand;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { discoverOIDC } from "../oauth/discoverOIDC";
|
|
2
|
+
import { revokeRefreshToken } from "../oauth/revokeToken";
|
|
3
|
+
import { type TokenStore } from "../oauth/tokenStore";
|
|
4
|
+
import type { CommonArgs } from "../cli/commonArgs";
|
|
5
|
+
import type { CommandDeps } from "../cli/types";
|
|
6
|
+
/** Verb-specific deps for `axe-auth logout`. */
|
|
7
|
+
export interface LogoutDeps extends CommandDeps {
|
|
8
|
+
/** Override the token store. Defaults to `KeyringTokenStore()`. */
|
|
9
|
+
tokenStore?: TokenStore;
|
|
10
|
+
/** Override OIDC discovery (for tests). */
|
|
11
|
+
discoverOIDC?: typeof discoverOIDC;
|
|
12
|
+
/** Override the revocation POST (for tests). */
|
|
13
|
+
revokeRefreshToken?: typeof revokeRefreshToken;
|
|
14
|
+
}
|
|
15
|
+
/** `axe-auth logout` — best-effort revoke + always clear local. */
|
|
16
|
+
declare const logoutCommand: {
|
|
17
|
+
name: string;
|
|
18
|
+
summary: string;
|
|
19
|
+
helpText: string;
|
|
20
|
+
options: {};
|
|
21
|
+
requiresConfig: false;
|
|
22
|
+
run(_args: CommonArgs, deps: LogoutDeps): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
export default logoutCommand;
|
|
@@ -0,0 +1,2 @@
|
|
|
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 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.";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP_LOGOUT = void 0;
|
|
4
|
+
const commonArgs_help_1 = require("../cli/commonArgs.help");
|
|
5
|
+
/** Help text for `axe-auth logout --help`. */
|
|
6
|
+
exports.HELP_LOGOUT = `axe-auth logout
|
|
7
|
+
|
|
8
|
+
Revoke the stored refresh token server-side (best-effort) and clear
|
|
9
|
+
the local OS keychain entry.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
axe-auth logout
|
|
13
|
+
|
|
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.
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
${commonArgs_help_1.HELP_COMMON_OPTIONS}
|
|
22
|
+
|
|
23
|
+
Behavior:
|
|
24
|
+
- If no tokens are stored, prints a note and exits 0 (logout is
|
|
25
|
+
idempotent).
|
|
26
|
+
- If the stored blob is unreadable (corrupt or from an
|
|
27
|
+
unsupported schema version), the local entry is cleared without
|
|
28
|
+
attempting server-side revocation, and a warning is printed.
|
|
29
|
+
- Otherwise: discovers the revocation endpoint at the stored
|
|
30
|
+
issuer, POSTs the stored refresh token per RFC 7009, then
|
|
31
|
+
clears the local keychain entry. Revocation failures or a
|
|
32
|
+
missing revocation endpoint are warned about; the local clear
|
|
33
|
+
still runs.
|
|
34
|
+
|
|
35
|
+
Exit codes:
|
|
36
|
+
0 Tokens cleared (server-side revocation may have failed; see
|
|
37
|
+
stderr for warnings).
|
|
38
|
+
2 Local clear failure.`;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const discoverOIDC_1 = require("../oauth/discoverOIDC");
|
|
4
|
+
const revokeToken_1 = require("../oauth/revokeToken");
|
|
5
|
+
const tokenStore_1 = require("../oauth/tokenStore");
|
|
6
|
+
const logout_help_1 = require("./logout.help");
|
|
7
|
+
const errors_1 = require("../cli/errors");
|
|
8
|
+
/** `axe-auth logout` — best-effort revoke + always clear local. */
|
|
9
|
+
const logoutCommand = {
|
|
10
|
+
name: "logout",
|
|
11
|
+
summary: "Revoke the stored refresh token server-side and clear the local keychain entry.",
|
|
12
|
+
helpText: logout_help_1.HELP_LOGOUT,
|
|
13
|
+
options: {},
|
|
14
|
+
requiresConfig: false,
|
|
15
|
+
async run(_args, deps) {
|
|
16
|
+
const discoverFn = deps.discoverOIDC ?? discoverOIDC_1.discoverOIDC;
|
|
17
|
+
const revokeFn = deps.revokeRefreshToken ?? revokeToken_1.revokeRefreshToken;
|
|
18
|
+
const tokenStore = deps.tokenStore ?? new tokenStore_1.KeyringTokenStore();
|
|
19
|
+
// Prefer the entry the dispatcher already loaded; only fall back
|
|
20
|
+
// to a fresh read when there isn't one (test path).
|
|
21
|
+
const loaded = deps.loadedEntry ?? (await tokenStore.load());
|
|
22
|
+
if (!loaded.ok) {
|
|
23
|
+
if (loaded.reason === "empty") {
|
|
24
|
+
deps.stdout.write("No stored credentials. Already logged out.\n");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
// Corrupt or version-mismatch: there IS a blob, just unusable.
|
|
28
|
+
// Server-side revocation isn't possible without a parseable
|
|
29
|
+
// refresh token, but the local entry must be cleared so the
|
|
30
|
+
// user actually ends up logged out.
|
|
31
|
+
deps.stderr.write(`axe-auth: stored credentials are unusable (${loaded.reason}); clearing local entry without server-side revocation.\n`);
|
|
32
|
+
await clearLocal(tokenStore);
|
|
33
|
+
deps.stdout.write("✓ Logged out.\n");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const { issuerURL, clientId, allowInsecureIssuer } = loaded.entry;
|
|
37
|
+
// Best-effort server-side revocation. Any failure here is a
|
|
38
|
+
// warning; the local clear runs unconditionally below so the
|
|
39
|
+
// user is "logged out" from this machine regardless.
|
|
40
|
+
if (loaded.entry.tokens.refreshToken) {
|
|
41
|
+
try {
|
|
42
|
+
const config = await discoverFn(issuerURL, { allowInsecureIssuer });
|
|
43
|
+
if (config.revocationEndpoint) {
|
|
44
|
+
await revokeFn({
|
|
45
|
+
revocationEndpoint: config.revocationEndpoint,
|
|
46
|
+
clientId,
|
|
47
|
+
refreshToken: loaded.entry.tokens.refreshToken,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
deps.stderr.write(`axe-auth: authorization server did not advertise a revocation endpoint; refresh token cleared locally only.\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
deps.stderr.write(`axe-auth: server-side revocation failed (${(0, errors_1.describeError)(err)}). Continuing with local clear.\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await clearLocal(tokenStore);
|
|
59
|
+
deps.stdout.write("✓ Logged out.\n");
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
async function clearLocal(tokenStore) {
|
|
63
|
+
try {
|
|
64
|
+
await tokenStore.clear();
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new errors_1.CLIError("KEYCHAIN_FAILURE", `Failed to clear stored credentials: ${(0, errors_1.describeError)(err)}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.default = logoutCommand;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getValidAccessToken } from "../oauth/getValidAccessToken";
|
|
2
|
+
import { type TokenStore } from "../oauth/tokenStore";
|
|
3
|
+
import type { CommonArgs } from "../cli/commonArgs";
|
|
4
|
+
import type { CommandDeps } from "../cli/types";
|
|
5
|
+
/** Verb-specific deps for `axe-auth token`. */
|
|
6
|
+
export interface TokenDeps extends CommandDeps {
|
|
7
|
+
/** Override `getValidAccessToken` so tests don't touch the keychain or network. */
|
|
8
|
+
getToken?: typeof getValidAccessToken;
|
|
9
|
+
/** Override the token store. Defaults to a fresh `KeyringTokenStore()`. */
|
|
10
|
+
tokenStore?: TokenStore;
|
|
11
|
+
}
|
|
12
|
+
/** `axe-auth token` — print a currently-valid access token to stdout. */
|
|
13
|
+
declare const tokenCommand: {
|
|
14
|
+
name: string;
|
|
15
|
+
summary: string;
|
|
16
|
+
helpText: string;
|
|
17
|
+
options: {};
|
|
18
|
+
requiresConfig: false;
|
|
19
|
+
run(_args: CommonArgs, deps: TokenDeps): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export default tokenCommand;
|
|
@@ -0,0 +1,2 @@
|
|
|
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 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).";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELP_TOKEN = void 0;
|
|
4
|
+
const commonArgs_help_1 = require("../cli/commonArgs.help");
|
|
5
|
+
/** Help text for `axe-auth token --help`. */
|
|
6
|
+
exports.HELP_TOKEN = `axe-auth token
|
|
7
|
+
|
|
8
|
+
Print a currently-valid OAuth access token to stdout, refreshing
|
|
9
|
+
silently against the stored refresh token if needed.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
axe-auth token
|
|
13
|
+
|
|
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
|
+
|
|
20
|
+
Options:
|
|
21
|
+
${commonArgs_help_1.HELP_COMMON_OPTIONS}
|
|
22
|
+
|
|
23
|
+
Output:
|
|
24
|
+
The access token is written to stdout with a trailing newline so
|
|
25
|
+
shell substitution (\`$(axe-auth token)\`) works cleanly. Nothing
|
|
26
|
+
else is written to stdout.
|
|
27
|
+
|
|
28
|
+
Security note:
|
|
29
|
+
Using \`$(axe-auth token)\` in a shell command exposes the access
|
|
30
|
+
token briefly in the system process-list (observable via \`ps\` on
|
|
31
|
+
POSIX, Task Manager on Windows). OAuth access tokens are
|
|
32
|
+
short-lived (typically minutes), which limits this exposure
|
|
33
|
+
compared to a static API key, but you should prefer streaming the
|
|
34
|
+
token into a file or env var on platforms where process listings
|
|
35
|
+
are sensitive.
|
|
36
|
+
|
|
37
|
+
Exit codes:
|
|
38
|
+
0 Success; the token was printed.
|
|
39
|
+
1 Not authenticated; run \`axe-auth login\` to re-authenticate.
|
|
40
|
+
2 Configuration error or transient failure (network, server,
|
|
41
|
+
keychain).`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const errors_1 = require("../oauth/errors");
|
|
4
|
+
const getValidAccessToken_1 = require("../oauth/getValidAccessToken");
|
|
5
|
+
const tokenStore_1 = require("../oauth/tokenStore");
|
|
6
|
+
const token_help_1 = require("./token.help");
|
|
7
|
+
const errors_2 = require("../cli/errors");
|
|
8
|
+
/** `axe-auth token` — print a currently-valid access token to stdout. */
|
|
9
|
+
const tokenCommand = {
|
|
10
|
+
name: "token",
|
|
11
|
+
summary: "Print a currently-valid access token to stdout, refreshing silently if needed.",
|
|
12
|
+
helpText: token_help_1.HELP_TOKEN,
|
|
13
|
+
options: {},
|
|
14
|
+
requiresConfig: false,
|
|
15
|
+
async run(_args, deps) {
|
|
16
|
+
const getToken = deps.getToken ?? getValidAccessToken_1.getValidAccessToken;
|
|
17
|
+
const tokenStore = deps.tokenStore ?? new tokenStore_1.KeyringTokenStore();
|
|
18
|
+
// Stored entry is the only source of truth: single-entry-per-machine
|
|
19
|
+
// model, the blob carries the issuer/client coordinates the tokens
|
|
20
|
+
// were minted against, and there is no longer a flag to override
|
|
21
|
+
// them with.
|
|
22
|
+
const loaded = deps.loadedEntry ?? (await tokenStore.load());
|
|
23
|
+
let token;
|
|
24
|
+
try {
|
|
25
|
+
token = await getToken({
|
|
26
|
+
issuerURL: loaded.ok ? loaded.entry.issuerURL : "",
|
|
27
|
+
clientId: loaded.ok ? loaded.entry.clientId : "",
|
|
28
|
+
allowInsecureIssuer: loaded.ok
|
|
29
|
+
? loaded.entry.allowInsecureIssuer
|
|
30
|
+
: false,
|
|
31
|
+
tokenStore,
|
|
32
|
+
loadedEntry: loaded,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err instanceof errors_1.OAuthFlowError && err.code === "NOT_AUTHENTICATED") {
|
|
37
|
+
throw new errors_2.CLIError("NOT_AUTHENTICATED", err.message);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
deps.stdout.write(`${token}\n`);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
exports.default = tokenCommand;
|