feat(auth): add actual login --device for browserless sign-in#802
Open
benw5483 wants to merge 2 commits into
Open
feat(auth): add actual login --device for browserless sign-in#802benw5483 wants to merge 2 commits into
actual login --device for browserless sign-in#802benw5483 wants to merge 2 commits into
Conversation
Add the OAuth 2.0 device-authorization grant (RFC 8628) as a `--device` flag on `actual login`, so a terminal with no local browser (remote dev, SSH, CI) can sign in by approving a short code on any other device. The flow mirrors the existing browser login. It requests a device + user code from `/api/oauth/device_authorization`, prints the verification URL and code to stderr for the user to approve, then polls `/api/oauth/token` with the device-code grant — honoring the server's interval and the `slow_down` backoff — until the session is issued. It resolves the identity via `/whoami` and persists the credentials through the existing encrypted store, so `logout`, `whoami`, and silent refresh keep working unchanged. It reuses the HTTPS/loopback transport guard and the `--api-url` / `ACTUAL_*` overrides; the requested scopes are the colon-form `adr:query adr:review mcp:invoke`. Unit tests mock both endpoints and cover the full poll state machine (pending, slow_down backoff, approval, denial, and both client- and server-side expiry), keeping src/auth/oauth.rs at 100% line coverage. The end-to-end run against the live server is deferred until the device-code endpoints reach staging. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The device-authorization grant (RFC 8628) is human-delegated — a person must approve the code + URL on the approval page — so it is not an unattended path. The `--device` help text listed "CI" as a use case; correct it to a human signing in from a remote/SSH shell with no local browser, and point unattended callers to `auth create-token` instead. Also flag an unverified assumption: the device grant drops `offline_access` and banks on the server returning a refresh token anyway. That has not been confirmed against the live endpoint, so the scope comment now says so rather than asserting it as fact. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 tasks
55cbff8 to
89d4933
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
actual login --device, the OAuth 2.0 device-authorization grant (RFC 8628), so a person at a shell with no local browser (remote dev, an SSH session) can sign in by approving a short code on another device. This is a browserless human-in-the-loop sign-in, not an unattended path — the device grant needs someone to approve the code, and CI has no approver (useactual auth create-tokenfor unattended agents / CI). It builds on the existingactual loginsession handling rather than adding a second credential store.POST /api/oauth/device_authorization, then prints the verification URL and short code to stderr for the user to approve on any device (stdout stays clean for scripting).POST /api/oauth/tokenwithgrant_type=urn:ietf:params:oauth:grant-type:device_code, honoring the server'sintervaland backing off onslow_down, until the session is issued or the code is denied/expires./whoamiand persists the session through the existing encrypted credential store — the same one the browser flow uses, sologout,whoami, and silent refresh all work unchanged.adr:query adr:review mcp:invoke. Reuses the HTTPS-only / loopback transport guard and the--api-url(andACTUAL_AUTH_URL/ACTUAL_OAUTH_*) overrides from the browser flow.sequenceDiagram actor User participant CLI as actual login --device participant Auth as /api/oauth/device_authorization participant Token as /api/oauth/token participant Who as /whoami CLI->>Auth: POST client_id + scope Auth-->>CLI: device_code, user_code, verification_uri, interval, expires_in CLI-->>User: "Visit <uri>, enter <user_code>" (stderr) loop every interval (slow_down adds 5s) until approved or expired CLI->>Token: POST device_code grant Token-->>CLI: authorization_pending / slow_down / session end CLI->>Who: GET whoami (Bearer access_token) Who-->>CLI: organization_id, member_id CLI->>CLI: persist encrypted sessionSecurity posture
The device flow handles a session credential, so the safety properties are worth stating explicitly:
http://is allowed only for loopback (localhost/127.0.0.1/[::1]) for local testing, reusing the browser flow's guard.0600), never to stdout or a log.Authorization, consent, and scope enforcement live entirely on the server (out of scope here); the client only drives the public grant and stores the result.
Test plan
cargo fmt --checkandcargo clippy -- -D warningsare clean./api/oauth/device_authorizationand/api/oauth/tokenand cover the full poll state machine: approval on the first poll,authorization_pending,slow_downbackoff,access_denied, serverexpired_token, client-side poll-budget expiry, unknown / non-JSON error bodies, and the HTTPS guard.src/auth/oauth.rsstays at 100% line coverage.actual loginwith no flag keeps the existing browser / loopback flow unchanged.Follow-ups
offline_accessand assumes the server returns a refresh token anyway; that is unverified against the live endpoint and should be confirmed (oroffline_accessrestored) when the endpoints reach staging.