Add --device flag for headless OAuth login and fix session token handling - #71
Conversation
…ndling ## Summary Add OAuth device flow as the preferred authentication path for remote and headless hosts, and fix two session-handling bugs: pasted 60-second Clerk session JWTs were rejected immediately due to incorrect refresh skew, and first-party Dev/staging API URLs incorrectly required explicit OAuth configuration in `config.toml`. Bumps the `am` CLI workspace to 0.3.0. ## Changes - Add `am auth login --device` for OAuth device flow that stores a refreshable Cloud session - Route `--no-browser` through device flow instead of browser OAuth with a loopback callback - Add `--issuer` override to the device login path, persisted so later commands can refresh the session - Fail closed when the browser cannot open: exit immediately with device-flow guidance rather than waiting on a dead loopback server - Fix token freshness (`token_fresh`): skip the 60-second refresh skew for paste-login sessions that have no refresh token — fixes `am init` with freshly pasted short-lived Clerk JWTs (ATO-2308) - Introduce `is_first_party_cloud_api_url` and use it in place of `is_production_api_url` in auth, doctor, and OAuth-pair resolution — Dev and staging API URLs now use the shipped OAuth pair without requiring `config.toml` entries (ATO-2321) - Validate device login OAuth overrides before the flow starts; refuse any override that later commands would ignore (first-party origins ignore configured pairs, `ATOMICMEMORY_OAUTH_ISSUER` outranks config) - Add `reject_blank_oauth_overrides` guard applied to both browser and device login paths - Fail closed when the device token response omits `refresh_token`; validate that the returned issuer matches the resolved issuer for the origin - Add `DeviceLoginOptions.issuer` and `skip_project_select` fields; remove `LoginOptions.no_browser` (routing handled by `select_auth_login_method`) - Export `headless_login_next_steps` for consistent error-path copy across auth commands - Update README: document `--device` as the preferred remote/headless path; clarify `--token` as a short-lived paste session - Bump the Rust CLI workspace version from 0.2.2 to 0.3.0 (`Cargo.toml`, `Cargo.lock`) for the `cli-v0.3.0` release ## Why Remote and VPS users previously had no refreshable login path — paste-login (`--token`) stores a short-lived JWT with no refresh token, requiring manual re-authentication. The device flow stores a proper OAuth session that CLI commands can refresh without user intervention. The token freshness fix (ATO-2308) addressed Clerk dashboard JWTs being rejected the moment they were pasted: their 60-second lifetime was shorter than the refresh skew applied unconditionally to all sessions, making `am init` unusable with a freshly issued token. The first-party Dev URL fix (ATO-2321) ensures `am auth doctor` and `am whoami` work against `api.dev.atomicstrata.ai` out of the box; the old `is_production_api_url` check excluded Dev and staging, forcing contributors and QA to add OAuth entries to `config.toml` that the prod path never needed. ## Validation - New integration test file `crates/cli/tests/device_login_oauth.rs` covers the full device poll loop against a local Axum mock, including the fail-closed case for an id-token-only response - Unit tests added for `oauth_tokens_from_device_response`, `device_oauth_pair`, `token_fresh`, `unrefreshable_session_error`, `select_auth_login_method`, `headless_login_next_steps`, and first-party Dev URL OAuth resolution - `pnpm run ci:rust` (fmt check, clippy `-D warnings`, full test suite, release `am --help` smoke)
ethanj
left a comment
There was a problem hiding this comment.
Reviewed PR #71 at 507e2ed. No outstanding blocking findings.
Confirmed the CLI implementation and regression tests match the previously reviewed source. The release metadata consistently bumps the Rust workspace, path dependencies, and lockfile package versions to 0.3.0.
The device flow validates the usable OAuth pair before requesting authorization, persists explicit overrides, rejects incompatible issuers and missing refresh tokens, and applies the same flag conflicts to --device and --no-browser. Both browser and device login reject blank OAuth overrides before configuration writes.
Verification: reused the seven focused tests previously run against identical implementation and test files, including byte-for-byte credential/config preservation and subsequent whoami checks. All 17 checks on this public PR head are green, including macOS, Windows, and fmt-clippy-test.
Disclosure: I contributed the initial blank-override guard and device regression test; this approval is not independent review of that contribution. The consolidated implementation was also reviewed by another reviewer.
Approved for merge. This does not merge the PR or publish the release.
Summary
Add OAuth device flow as the preferred authentication path for remote and headless hosts, and fix two session-handling bugs: pasted 60-second Clerk session JWTs were rejected immediately due to incorrect refresh skew, and first-party Dev/staging API URLs incorrectly required explicit OAuth configuration in
config.toml. Bumps theamCLI workspace to 0.3.0.Changes
am auth login --devicefor OAuth device flow that stores a refreshable Cloud session--no-browserthrough device flow instead of browser OAuth with a loopback callback--issueroverride to the device login path, persisted so later commands can refresh the sessiontoken_fresh): skip the 60-second refresh skew for paste-login sessions that have no refresh token — fixesam initwith freshly pasted short-lived Clerk JWTs (ATO-2308)is_first_party_cloud_api_urland use it in place ofis_production_api_urlin auth, doctor, and OAuth-pair resolution — Dev and staging API URLs now use the shipped OAuth pair without requiringconfig.tomlentries (ATO-2321)ATOMICMEMORY_OAUTH_ISSUERoutranks config)reject_blank_oauth_overridesguard applied to both browser and device login pathsrefresh_token; validate that the returned issuer matches the resolved issuer for the originDeviceLoginOptions.issuerandskip_project_selectfields; removeLoginOptions.no_browser(routing handled byselect_auth_login_method)headless_login_next_stepsfor consistent error-path copy across auth commands--deviceas the preferred remote/headless path; clarify--tokenas a short-lived paste sessionCargo.toml,Cargo.lock) for thecli-v0.3.0releaseWhy
Remote and VPS users previously had no refreshable login path — paste-login (
--token) stores a short-lived JWT with no refresh token, requiring manual re-authentication. The device flow stores a proper OAuth session that CLI commands can refresh without user intervention.The token freshness fix (ATO-2308) addressed Clerk dashboard JWTs being rejected the moment they were pasted: their 60-second lifetime was shorter than the refresh skew applied unconditionally to all sessions, making
am initunusable with a freshly issued token.The first-party Dev URL fix (ATO-2321) ensures
am auth doctorandam whoamiwork againstapi.dev.atomicstrata.aiout of the box; the oldis_production_api_urlcheck excluded Dev and staging, forcing contributors and QA to add OAuth entries toconfig.tomlthat the prod path never needed.Validation
crates/cli/tests/device_login_oauth.rscovers the full device poll loop against a local Axum mock, including the fail-closed case for an id-token-only responseoauth_tokens_from_device_response,device_oauth_pair,token_fresh,unrefreshable_session_error,select_auth_login_method,headless_login_next_steps, and first-party Dev URL OAuth resolutionpnpm run ci:rust(fmt check, clippy-D warnings, full test suite, releaseam --helpsmoke)