@deque/axe-auth 1.4.0 → 1.5.0-next.7a5a136d
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 +43 -15
- package/credits.json +74 -0
- package/dist/cli/errors.d.ts +1 -1
- package/dist/cli/errors.js +1 -0
- package/dist/commands/run.d.ts +16 -0
- package/dist/commands/run.help.d.ts +2 -0
- package/dist/commands/run.help.js +41 -0
- package/dist/commands/run.js +108 -0
- package/dist/index.js +18 -2
- package/dist/oauth/keyringBinding.d.ts +1 -1
- package/dist/oauth/keyringBinding.js +3 -4
- package/dist/run/findFreePort.d.ts +17 -0
- package/dist/run/findFreePort.js +39 -0
- package/dist/run/pushToken.d.ts +10 -0
- package/dist/run/pushToken.js +35 -0
- package/dist/run/runSession.d.ts +62 -0
- package/dist/run/runSession.js +219 -0
- package/dist/run/supervise.d.ts +72 -0
- package/dist/run/supervise.js +234 -0
- package/dist/run/testUtils.d.ts +15 -0
- package/dist/run/testUtils.js +43 -0
- package/docs/architecture.md +63 -2
- package/docs/callback-server.md +9 -9
- package/package.json +6 -4
package/docs/architecture.md
CHANGED
|
@@ -15,6 +15,7 @@ flowchart TB
|
|
|
15
15
|
keychain[(OS keychain)]
|
|
16
16
|
axe[axe server]
|
|
17
17
|
keycloak[Customer Keycloak]
|
|
18
|
+
mcp[axe MCP server]
|
|
18
19
|
|
|
19
20
|
user -- "axe-auth login / token / logout" --> cli
|
|
20
21
|
cli -- "GET /api/sso-config (login only)" --> axe
|
|
@@ -27,19 +28,21 @@ flowchart TB
|
|
|
27
28
|
cli <-- "OIDC discovery, token exchange,<br/>refresh, revoke (HTTPS)" --> keycloak
|
|
28
29
|
cli <-- "tokens + issuer/client/walnutURL<br/>(versioned blob)" --> keychain
|
|
29
30
|
cli -- "access token (stdout)" --> user
|
|
31
|
+
cli -- "axe-auth run: spawn + supervise,<br/>access-token push (loopback POST /token)" --> mcp
|
|
30
32
|
```
|
|
31
33
|
|
|
32
34
|
**Components and their roles:**
|
|
33
35
|
|
|
34
|
-
1. **Developer**: invokes `axe-auth login`, `axe-auth token`, or `axe-auth logout` on their host machine.
|
|
36
|
+
1. **Developer**: invokes `axe-auth login`, `axe-auth token`, or `axe-auth logout` on their host machine. `axe-auth run` is not typed directly; the developer configures it as their MCP client's server command, and the client spawns the CLI over stdio (see [`axe-auth run`](#axe-auth-run) below).
|
|
35
37
|
2. **axe-auth CLI**: this package. Drives the OAuth flow, persists tokens, prints access tokens on stdout, revokes refresh tokens on logout.
|
|
36
38
|
3. **System browser**: the developer's default OS browser (Chrome, Safari, Firefox, etc.). Used only for the user-interactive part of the OAuth Authorization Code flow. Runs on the host, never in a container or sandbox controlled by `axe-auth`.
|
|
37
39
|
4. **Loopback callback server**: an HTTP listener bound to `127.0.0.1` on an OS-assigned ephemeral port. Spawned by the CLI at the start of `login` and torn down as soon as the OAuth callback fires. Per RFC 8252 §7.3, this is the standard pattern for native-app OAuth.
|
|
38
40
|
5. **OS keychain**: the platform-native credential store accessed through [`@napi-rs/keyring`](https://www.npmjs.com/package/@napi-rs/keyring) — macOS Keychain, Windows Credential Manager, or Linux Secret Service (GNOME Keyring, KWallet). The CLI writes one entry per machine.
|
|
39
41
|
6. **axe server**: the customer's deployment of the axe API. The CLI hits its `/api/sso-config` endpoint at the start of `login` to discover the Keycloak URL, realm, and OAuth client ID; no other CLI traffic flows through the axe server.
|
|
40
42
|
7. **Customer Keycloak**: the OAuth authorization server for the customer's deployment. Issues access and refresh tokens. Federation between Keycloak and any upstream enterprise IdP (Okta, AAD, etc.) is the customer's concern and out of scope for this document.
|
|
43
|
+
8. **axe MCP server**: the server that `axe-auth run` launches and supervises. It receives freshly-minted access tokens pushed over loopback (`POST /token`) so a long session survives token expiry without a restart. Present only in the `run` flow, not in `login`/`token`/`logout`.
|
|
41
44
|
|
|
42
|
-
`axe-auth` itself does **not** communicate with Deque's API services directly. The access tokens it produces are consumed by downstream tools, most notably the axe MCP server, which presents them to Deque's services as `Authorization: Bearer ...`.
|
|
45
|
+
`axe-auth` itself does **not** communicate with Deque's API services directly. The access tokens it produces are consumed by downstream tools, most notably the axe MCP server, which presents them to Deque's services as `Authorization: Bearer ...`. The one exception is `axe-auth run`, which supervises a running axe MCP server and pushes freshly-minted access tokens to it over a loopback (`127.0.0.1`) connection so a long session survives token expiry without a restart; it never sends the refresh token, only short-lived access tokens.
|
|
43
46
|
|
|
44
47
|
## Data flow per verb
|
|
45
48
|
|
|
@@ -119,6 +122,64 @@ sequenceDiagram
|
|
|
119
122
|
5. On success, the CLI persists the rotated entry and prints the new access token on stdout.
|
|
120
123
|
6. On `invalid_grant` (refresh token revoked or expired server-side), the CLI clears the local entry and exits 1 with a "re-authenticate" message on stderr. Other transient failures (network, 5xx) leave the stored entry intact so the user can retry.
|
|
121
124
|
|
|
125
|
+
### `axe-auth run`
|
|
126
|
+
|
|
127
|
+
```mermaid
|
|
128
|
+
sequenceDiagram
|
|
129
|
+
participant User as Developer
|
|
130
|
+
participant Client as MCP client
|
|
131
|
+
participant CLI as axe-auth run
|
|
132
|
+
participant KC as Customer Keycloak
|
|
133
|
+
participant Server as axe MCP server
|
|
134
|
+
|
|
135
|
+
User->>Client: set axe-auth run as the stdio server command
|
|
136
|
+
Client->>CLI: spawn (stdio)
|
|
137
|
+
alt --port or AXE_TOKEN_REFRESH_PORT pinned
|
|
138
|
+
Note over CLI: use it as given (required for a container runtime)
|
|
139
|
+
else nothing pinned
|
|
140
|
+
Note over CLI: take a free loopback port from the OS,<br/>so a leftover server cannot collide
|
|
141
|
+
end
|
|
142
|
+
Note over CLI: mint initial access token (refresh via Keycloak if near expiry)
|
|
143
|
+
opt token near expiry
|
|
144
|
+
CLI->>KC: POST /token (refresh_token)
|
|
145
|
+
KC-->>CLI: { access_token, expires_in }
|
|
146
|
+
end
|
|
147
|
+
CLI->>Server: spawn child (AXE_ACCESS_TOKEN, refresh port + secret injected)
|
|
148
|
+
Client->>CLI: MCP JSON-RPC (stdin)
|
|
149
|
+
CLI->>Server: bridged stdin
|
|
150
|
+
Server->>CLI: bridged stdout
|
|
151
|
+
CLI->>Client: MCP JSON-RPC (stdout)
|
|
152
|
+
loop each interval, before expiry
|
|
153
|
+
CLI->>KC: POST /token (refresh_token)
|
|
154
|
+
KC-->>CLI: { access_token }
|
|
155
|
+
CLI->>Server: POST /token (x-refresh-secret) — swap in-memory token
|
|
156
|
+
end
|
|
157
|
+
alt wrapped server exits on its own
|
|
158
|
+
Server-->>CLI: child exits
|
|
159
|
+
CLI-->>Client: exit with the child's code
|
|
160
|
+
else session ends (stdin EOF, forwarded signal, launcher gone, or fatal startup abort)
|
|
161
|
+
CLI->>Server: polite rung — forwarded signal or SIGTERM across the process group, or a closed stdin on Windows
|
|
162
|
+
opt still alive after the grace period
|
|
163
|
+
CLI->>Server: SIGKILL (process group, or taskkill /T /F on Windows)
|
|
164
|
+
end
|
|
165
|
+
Server-->>CLI: child exits
|
|
166
|
+
end
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
1. The developer points their MCP client at `axe-auth run -- <server launch command>` as the stdio server command. A free loopback refresh port is taken from the OS per session, so a server left behind by an earlier session cannot collide with it; `--port` or `AXE_TOKEN_REFRESH_PORT` pins one instead. A container only reaches a host port it was told to publish with `-p`, so `run` refuses to guess one when it recognises the wrapped command as `docker`, `podman`, or `nerdctl`. Detection is by command name, so another runtime, or one reached through a wrapper script, takes an auto-selected port it cannot reach and degrades to no token refresh; pin a port yourself there.
|
|
170
|
+
2. `run` obtains a currently-valid access token (exactly as `axe-auth token` does, refreshing against Keycloak if needed), generates a shared secret unless one is provided, and launches the wrapped server as a child process with the token, port, and secret injected into its environment.
|
|
171
|
+
3. `run` transparently bridges the client's stdio to the child so the MCP session flows through untouched, and supervises the child for the session's lifetime.
|
|
172
|
+
4. In the background, `run` keeps the access token fresh and pushes each new token to the server's loopback listener at `http://127.0.0.1:<port>/token` (secret in an `x-refresh-secret` header). The refresh token is never sent; only short-lived access tokens.
|
|
173
|
+
5. When the wrapped server exits on its own, `run` exits with the same code.
|
|
174
|
+
6. `run` also ends the session itself, so the server cannot outlive it holding the refresh port: on stdin EOF, on a forwarded `SIGINT`/`SIGTERM`/`SIGHUP`, when the launching process disappears, and on a fatal error. Each puts the child on a teardown ladder: a polite rung the server can shut down on, then a forced one. On macOS and Linux that is the forwarded signal (or `SIGTERM`), then `SIGKILL` across the process group. Windows has no polite signal, so the closed stdin pipe is the polite rung and the forced one is `taskkill /T /F` across the process tree. The wrapped server separately ends the session when the client stops answering `ping`, which catches a client that never closed the pipe.
|
|
175
|
+
|
|
176
|
+
#### Known limitations
|
|
177
|
+
|
|
178
|
+
Two cases are bounded rather than closed.
|
|
179
|
+
|
|
180
|
+
- **A container can outlive an ungraceful teardown.** When the wrapped command is a container runtime, `axe-auth` supervises the client, not the container, so the polite rung is forwarded to the container but `SIGKILL` reaches only the client. A server that does not stop within the grace therefore leaves the container running on the port it published, which the next session cannot reuse. Recover with `docker rm -f <container>`. Pinning a port is already required here, so the collision is not silent. Tracked in [#1027](https://github.com/dequelabs/axe-mcp-server/issues/1027).
|
|
181
|
+
- **A browser can outlive an ungraceful teardown.** On POSIX the final `SIGKILL` reaches the server's process group, but Playwright runs Chromium in its own, so a scan still mid-flight when the grace expires leaves a browser for the user to close by hand. It holds no port, so it does not block the next session. Windows is unaffected: the forced rung there walks the whole process tree.
|
|
182
|
+
|
|
122
183
|
### `axe-auth logout`
|
|
123
184
|
|
|
124
185
|
```mermaid
|
package/docs/callback-server.md
CHANGED
|
@@ -10,12 +10,12 @@ Request handling additionally rejects non-loopback `remoteAddress` values with `
|
|
|
10
10
|
|
|
11
11
|
## RFC 8252 conformance
|
|
12
12
|
|
|
13
|
-
| Clause | Requirement
|
|
14
|
-
|
|
|
15
|
-
| §7.3
|
|
16
|
-
| §7.3
|
|
17
|
-
| §7.3
|
|
18
|
-
| §8.3
|
|
19
|
-
| §8.3
|
|
20
|
-
| §8.1
|
|
21
|
-
| §8.1
|
|
13
|
+
| Clause | Requirement | Handled by |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| §7.3 | IP literal, not `localhost` | Binds `127.0.0.1` or `[::1]`; redirect URI uses the literal. |
|
|
16
|
+
| §7.3 | Ephemeral OS-assigned port | `listen(0, ...)`; port read from `listening` event. |
|
|
17
|
+
| §7.3 | Attempt both IPv4 and IPv6 | IPv4-first, IPv6 fallback when the IPv4 family is unavailable. |
|
|
18
|
+
| §8.3 | Open the port only during the auth request | One-shot: closed on first consuming request, timeout, or abort. |
|
|
19
|
+
| §8.3 | Listen on loopback only | IP literal only; `remoteAddress` check as defense in depth. |
|
|
20
|
+
| §8.1 | PKCE | Out of scope — owned by the auth-URL + token-exchange layer. |
|
|
21
|
+
| §8.1 | Auth code interception mitigation | Success HTML does not echo `code`; CSP locks the page down (see [`callback-page.md`](./callback-page.md)). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deque/axe-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-next.7a5a136d",
|
|
4
4
|
"description": "CLI authentication utility for Deque services",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -31,15 +31,17 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@napi-rs/keyring": "^1.3.0",
|
|
34
|
+
"cross-spawn": "^7.0.6",
|
|
34
35
|
"remove-trailing-slash": "^0.1.1",
|
|
35
36
|
"shlex": "^3.0.0",
|
|
36
37
|
"ts-dedent": "^2.2.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@hono/node-server": "^1.
|
|
40
|
+
"@hono/node-server": "^2.1.0",
|
|
41
|
+
"@types/cross-spawn": "^6.0.6",
|
|
40
42
|
"@types/node": "^24.13.2",
|
|
41
|
-
"c8": "^
|
|
42
|
-
"hono": "^4.12.
|
|
43
|
+
"c8": "^12.0.0",
|
|
44
|
+
"hono": "^4.12.34",
|
|
43
45
|
"tsx": "^4.22.4",
|
|
44
46
|
"typescript": "^6.0.3"
|
|
45
47
|
},
|