auth: sign in to a Fjord Account under SSO/OIDC (#218) #219
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/218-fjord-oidc-login"
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?
Fixes #218.
fj auth login --fjordbailed with a StrategyMismatch error on any deployment whose sign-in strategy isoidc(the production shape fromGET /api/auth/status), so every Fjord-Account command was unusable there and a Forgejo PAT was the only working path.This drives the sign-in off
GET /api/auth/statusand adds the OIDC flow Paragon actually exposes: OAuth 2.0 Authorization Code + PKCE (S256) over a loopback redirect. Paragon's discovery advertisesauthorization_code+refresh_tokenwithS256and no device-authorization endpoint, so loopback + PKCE is the supported shape, not device code.fjsigns in as thefj-clipublic client through the platform's/oauth/*front door (the same one the iOS app uses). The access token the token endpoint returns is a Paragon session bearer that/api/v1/me/forge-instancesalready accepts, so it flows through the existing gateway unchanged; the refresh token enables silent renewal.What changed
src/fjord/oidc.rs:/api/auth/statusprobe, PKCE, authorize-URL builder, one-shot loopback listener, code + refresh token exchange, and keychain storage.fj auth login --fjordbranches on the probed strategy. The email + password flow is unchanged and remains the fallback forcredentialsdeployments; a probe failure still falls through to it.fj auth refreshrenews an OIDC session silently from its stored refresh token;fj instancesrefreshes and retries once on a 401 (SessionExpired).fj auth logout.fj auth statusshows the sign-in method.getrandom+ringpromoted to direct deps for the PKCE verifier + S256 challenge. Both were already in the tree via reqwest's rustls stack, so the lockfile gains no new crates.Deployment prerequisite
Paragon must register the
fj-clipublic OIDC client withallowed_grant_types = ["authorization_code", "refresh_token"]and these exact loopback redirect URIs (Paragon matches redirect URIs exactly, with no RFC 8252 loopback-port exemption):fjbinds the first free port in that list. The client id and (for staging) an alternate can be overridden withFJ_OIDC_CLIENT_ID. Until the client is registered, the PAT path keeps working, so this is not launch-blocking.Checks
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo test --all(657 pass; new unit + wiremock tests mock the status/token/refresh endpoints)cargo audit(known unmaintained-only ignores, unchanged)f7f8c65363315b9490e0Forseti review
1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet).
Findings
P1
src/fjord/oidc.rs:459— The refresh token key is scoped only by platform URL, butstore_tokensintentionally keeps any already-stored refresh token when the new token response omitsrefresh_token.login_fjord_oidctells the user that no silent refresh is available in that case, yet a previous refresh token for the same platform can remain in the keychain. When the current access token later gets a 401,refresh_sessionloads that stored refresh token and exchanges it, which can silently restore a prior Fjord Account session rather than the account that just signed in.PR:
rasterstate/fj#219Head SHA:
315b9490e09cReview job:
sha256:c4f59ccfadd8e32ba4ad626374bcbea639de0e08695969203606fc21ce4ea094Provider pair:
openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001Blocking findings:
1Inline findings:
1Token source:
GITHUB_TOKENRunner:
ae4eb40225f3Run: https://rasterhub.com/rasterstate/fj/actions/runs/366
Forseti review
1 blocking finding(s) from the lead reviewer (advisory: not gating merges yet).
Findings
P1
src/fjord/oidc.rs:578—refresh_sessionloads the currently stored refresh token and immediately exchanges it without any per-platform/process lock. The new automatic retry path can call this from multiplefj instancesprocesses at the same time after both see a 401. Since the code documents that Paragon rotates refresh tokens and detects replay, two concurrent refreshes can both read the same old token; the first rotates and persists it, while the second replays the old token and can fail or invalidate the refreshed session. That turns a normal expired access token into a broken Fjord Account session until the user signs in again.PR:
rasterstate/fj#219Head SHA:
c97b2825fdecReview job:
sha256:65cc865e4157694dbe012a70af97ecb30f37343fd7140756aa409c11d14028f6Provider pair:
openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001Blocking findings:
1Inline findings:
1Token source:
GITHUB_TOKENRunner:
2bc6c173e4dbRun: https://rasterhub.com/rasterstate/fj/actions/runs/368