# @deque/axe-auth

CLI for authenticating with Deque services via the OAuth 2.0 Authorization Code + PKCE flow (RFC 6749, RFC 7636, RFC 8252 §7.3). Tokens are persisted to the OS keychain so subsequent invocations can refresh silently.

## Installation

```sh
npm install -g @deque/axe-auth
```

Or run directly:

```sh
npx @deque/axe-auth
```

## Usage

```sh
axe-auth <command> [options]
```

Commands:

| Command | Description |
| --- | --- |
| `login` | Open a browser, complete the OAuth flow, persist tokens to the OS keychain. |
| `logout` | Revoke the stored refresh token server-side and clear the local keychain entry. |
| `token` | Print a currently-valid access token to stdout, refreshing silently if needed. |
| `run` | Launch and supervise the axe MCP server, keeping its OAuth token fresh with no restart. |

Run `axe-auth <command> --help` for command-specific options.

### Common configuration

`axe-auth` discovers its OAuth coordinates by calling `<server>/api/sso-config` on the axe server. Users only supply (or default to) the axe server URL — never the underlying Keycloak URL, realm, or client ID.

| Flag | Env var | Notes |
| --- | --- | --- |
| `--server` | `AXE_SERVER_URL` | axe server URL. Defaults to `https://axe.deque.com` (SaaS prod) when neither flag nor env var is set, so SaaS users pass no flags at all. Customers on other deployments override with their own axe server URL. |
| `--allow-insecure-issuer` | — | Permit non-loopback http URLs (default is https only; loopback http is always allowed). Applies to `login` only; `token` and `logout` use the policy persisted at login. |
| `--no-allow-insecure-issuer` | — | Force `allowInsecureIssuer=false` for the new `login` (and the entry it persists). Mutually exclusive with `--allow-insecure-issuer`. `token` and `logout` ignore this flag. |

`axe-auth` stores one set of credentials per machine. On a successful `login`, the discovered issuer / client / insecure-issuer values are persisted alongside the tokens, so subsequent `axe-auth token` and `axe-auth logout` invocations work flag-free — a typical scripted call is just `$(axe-auth token)`.

There is no concurrent multi-issuer support. Logging in to a second deployment overwrites the previous tokens; an interactive prompt confirms the switch before destroying the existing session, and `--force` skips the prompt.

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Success. |
| `1` | Not authenticated: `axe-auth token` with no stored credentials, or stored credentials are unusable (corrupt, version-mismatch, expired without a usable refresh token, or refresh rejected by the server). Branch on this in scripts that need to trigger a `login`. |
| `2` | Usage or runtime error: unknown command, bad flag, missing required configuration, OAuth flow failure, or keychain failure. Details written to stderr. |
| `3` | Cancelled: `axe-auth login` declined at the re-authentication prompt. Distinct from `1` so scripts can tell "needs login" from "user bailed." |

### Examples

```sh
# First-time login on Deque SaaS prod (opens your browser, no flags needed)
axe-auth login

# First-time login on a non-SaaS-prod deployment
axe-auth login --server https://axe.customer.dequecloud.com

# Pull a fresh access token for use in shell substitution
docker run -e AXE_ACCESS_TOKEN="$(axe-auth token)" axe-mcp-server

# Sign out
axe-auth logout
```

### Long-running sessions

Access tokens are short-lived, so a session that outlives the token's TTL (an agent driving the MCP server for hours, say) would otherwise start seeing auth failures. `axe-auth run` handles this automatically: it launches and supervises the server, then keeps its access token fresh for the whole session with no restart and no manual step. Point your MCP client at `axe-auth run` as the server command; it manages the server process's lifetime like any stdio server. It also shuts the server down when the session ends: on stdin close, on a termination signal, and when the launching process disappears. The server itself exits when the client stops answering its liveness probes. Each session takes a fresh refresh port from the OS unless you pin one with `--port` or `AXE_TOKEN_REFRESH_PORT`, so a server left behind by an earlier session cannot hold the port this one needs. Pin one for containers, which only reach a port you publish: `run` detects `docker`, `podman`, and `nerdctl` and refuses to guess, but any other runtime or a wrapper script is yours to pin. Your refresh token never leaves the machine; only short-lived access tokens are sent to the server.

The server just needs token refresh enabled on a port; `run` supplies the token and secret. Under Docker, publish the port on host loopback, forward the auth vars, and bind the listener to `0.0.0.0` inside the container so the published port can reach it:

```sh
AXE_TOKEN_REFRESH_PORT=9223 npx @deque/axe-auth run -- \
  docker run -i --rm \
    -p 127.0.0.1:9223:9223 \
    -e AXE_ACCESS_TOKEN \
    -e AXE_TOKEN_REFRESH_PORT \
    -e AXE_TOKEN_REFRESH_SECRET \
    -e AXE_TOKEN_REFRESH_HOST=0.0.0.0 \
    dequesystems/axe-mcp-server
```

`AXE_TOKEN_REFRESH_HOST=0.0.0.0` is required under Docker: a published port forwards to the container's network interface, not its loopback, so the default loopback bind would be unreachable. The `-p 127.0.0.1:9223:9223` publish keeps the endpoint off the host's external interfaces, and the shared secret — not container isolation — is what guards the endpoint itself.

Under npm the wrapped process inherits the values directly and the listener stays on loopback:

```sh
npx @deque/axe-auth run -- npx axe-mcp-server
```

`run` picks a free loopback port per session; pin one with `AXE_TOKEN_REFRESH_PORT` or `--port` only for containers, which can reach only a port you publish. `run` generates the shared secret unless you pin one with `AXE_TOKEN_REFRESH_SECRET` / `--secret`. The secret authenticates the push; the server's listener binds loopback by default and never starts without a secret.

## Architecture

See [`docs/architecture.md`](./docs/architecture.md) for the system architecture: components, per-verb data flow, communication security, and persisted data.

Deeper design notes:

- [`oauth-flow.md`](./docs/oauth-flow.md) — protocol-level walkthrough of the OAuth 2.0 + PKCE flow.
- [`callback-server.md`](./docs/callback-server.md) — `startCallbackServer` API and RFC 8252 conformance.
- [`callback-page.md`](./docs/callback-page.md) — HTML response design, branding, and CSP rationale.

## Caveats

- **`axe-auth token` exposes the access token in the shell process list and terminal scrollback.** When used as `$(axe-auth token)` the token briefly appears in the parent process's argument list (observable via `ps`); printed directly to a terminal it also persists in the scrollback buffer (iTerm2, Terminal.app, tmux all retain output by default), which can outlast the token's TTL on a shared machine. OAuth access tokens are short-lived (typically minutes), which limits exposure compared to a static API key. Prefer redirecting into a file (`axe-auth token > /tmp/tok && chmod 600 /tmp/tok`) or directly into an env var (`export AXE_ACCESS_TOKEN=$(axe-auth token)`) on platforms where this matters.
- **Linux keychain support is untested.** `@napi-rs/keyring` requires a working D-Bus Secret Service (GNOME Keyring, KWallet, etc.). Users on headless or minimal-desktop Linux environments may see `KEYRING_UNAVAILABLE`; a file-backed `TokenStore` fallback is tracked as a follow-up (internal issue #464).
