auth: durable cross-device Fjord sign-in via OAuth device flow (--device) #226
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/oidc-device-flow"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements the OAuth 2.0 Device Authorization Grant client (RFC 8628) so a user whose browser is on a different device than
fj(an iPad whilefjruns on a Mac/server, or fully headless) can sign in to a Fjord Account. Consumes the merged Paragon OP side (paragon #863), re-exposed by fjord-platform at/oauth/device_authorizationand/oauth/token. This is the durable replacement for the--manualpaste interim for the cross-device case.What the user sees
fj auth login --fjord --device:The QR is optional-friendly: it degrades to the plain URL if rendering fails (
render_qrreturnsNone).Flow
device_authorization(formclient_id=fj-cli, optionalscope), parse thedevice_code/user_code/ verification URIs /expires_in/interval./oauth/token(grant_type=urn:ietf:params:oauth:grant-type:device_code,client_id,device_code) everyintervalseconds, honoringauthorization_pending(keep polling),slow_down(+5s),access_denied/expired_token/invalid_grant(abort with a clear message), and theexpires_indeadline. A non-OAuth failure (proxy 502, HTML body) aborts rather than hot-looping.200, persist through the existingstore_tokenspath, so the session is identical to the loopback one: same keychain slots (bearer + identity-bound refresh), silent refresh available, andfj auth statusshows the Fjord Account. Public client, no secret, no PKCE (the device code binds the exchange).Auto-fallback
Device flow is auto-selected when no local browser is reachable (a
$SSH_CONNECTION/$SSH_TTYsession, or a Unix session with noDISPLAY/WAYLAND_DISPLAY); loopback stays the desktop default.FJ_FORCE_LOOPBACK=1opts back out. Explicit--device/--manualalways win (they conflict at the CLI layer). The intro line explains the auto-choice and how to override.Tests
cargo fmt+clippy -D warnings+ full suite green (678 passing). New wiremock coverage for both endpoints: device_authorization parse, each poll outcome (pending / slow_down / access_denied / expired_token / invalid_grant / approved), a non-OAuth failure, and the loop's happy path, slow_down back-off, denied/expired aborts, and client-side deadline timeout. Pure-unit tests forchoose_oidc_captureprecedence and QR rendering.Forseti review
No blocking findings from the lead reviewer.
Findings
P2
src/fjord/oidc.rs:815— The loop checksInstant::now() >= deadlinebefore sleeping, then always sleeps the full poll interval and callspoll_device_tokenafterward. If the remaining lifetime is shorter than the interval, the client will wait past the advertised expiration and still make a token request after the deadline instead of stopping whenexpires_inelapses. This weakens the promised client-side deadline behavior and can make short-lived or slowed-down device codes wait longer than intended before reporting expiry.PR:
rasterstate/fj#226Head SHA:
8a3b165f076dReview job:
sha256:30ee45c756b7021377b8eb32c10a8119026850115564d1b410586e45b44a4b58Provider pair:
openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001Blocking findings:
0Inline findings:
1Token source:
GITHUB_TOKENRunner:
8073074ba165Run: https://rasterhub.com/rasterstate/fj/actions/runs/377
Forseti review
No blocking findings from the lead reviewer.
Findings
P2
src/fjord/oidc.rs:805— The loop checksInstant::now() >= deadlineonly beforetokio::time::sleep(interval). If the remaining lifetime is shorter thaninterval—for example afterslow_down, a shortexpires_in, or a server-provided interval larger than the remaining time—the client sleeps past the deadline and then still callspoll_device_tokenonce before noticing expiry on the next iteration. That contradicts the stated client-side deadline behavior and can make the CLI hang longer than the code lifetime.PR:
rasterstate/fj#226Head SHA:
3aaa970d975cReview job:
sha256:1f3881cf4caf575237f18e41a0224270645c543c9ef1791856359b27c9b02e73Provider pair:
openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001Blocking findings:
0Inline findings:
1Token source:
GITHUB_TOKENRunner:
479cae1bae02Run: https://rasterhub.com/rasterstate/fj/actions/runs/379