auth: manual/no-browser paste mode for Fjord OIDC login (--manual) + branded callback page #225

Merged
stephen merged 1 commit from feat/224-manual-fjord-login into main 2026-07-21 19:39:49 +00:00
Owner

Fixes #224. On an SSO/OIDC deployment (production), fj auth login --fjord captures the sign-in redirect on a 127.0.0.1 loopback listener. That only works when the browser is on the same machine as fj: sign in from an iPad or against a remote server and the redirect to 127.0.0.1:47017/callback can't reach the listener, so sign-in can't complete. (Confirmed live: an iPad redirect failed to load but the code was still copyable from Safari's address bar.)

This adds --manual (alias --no-browser) to keep the whole Authorization Code + PKCE flow and change only how the code is obtained: instead of binding a listener, fj prints the authorize URL, the user signs in on any device, and pastes the redirected URL (or the bare code) back. No platform or Paragon changes: fj-cli is already registered with the loopback redirect, so manual mode pins the first registered URI (http://127.0.0.1:47017/callback) for the exact-match token exchange.

This is the interim path; OAuth device flow (separate work) is the durable replacement.

What changed

  • --manual / --no-browser on fj auth login (Fjord mode only). login_fjord_oidc splits into loopback vs manual code acquisition sharing PKCE, the token exchange, and persistence.
  • Manual mode parses the paste (src/fjord/oidc.rs::parse_pasted_callback): a full redirected URL, just its code=…&state=… query, or a bare code. The returned state is CSRF-checked when present (PastedCode::check_state); a bare code has no state to check and relies on the PKCE verifier (held only by this process) plus the code's single use and short TTL.
  • Terminal copy for manual sign-in: numbered steps, and it says up front the browser will show a can't-connect error on the 127.0.0.1 page, which is expected.
  • Branded the loopback callback page (write_responserender_callback_page), previously a bare <h1>fj</h1>. Self-contained HTML: inline styles and the inlined Fjord mark (the page is served off the loopback with no network, so no external asset would load), light/dark aware via prefers-color-scheme, palette mirrored from the fjord-platform design tokens. Success / failure / state-mismatch / not-found variants. Manual mode never reaches this page (it binds no listener); it's for the loopback flow.

Not covered / notes

  • Manual mode still targets the same /oauth/authorize + /oauth/token front door; nothing server-side changes.
  • The branded page can't show the account email (the code hasn't been exchanged yet at render time); the terminal success line already prints it.

Checks

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings (clean)
  • cargo test (666 pass, 2 ignored; new unit tests cover URL/query/bare-code parsing, empty/error/codeless rejection, the state CSRF-check, a mocked manual exchange end-to-end, and branded-page invariants)
Fixes #224. On an SSO/OIDC deployment (production), `fj auth login --fjord` captures the sign-in redirect on a `127.0.0.1` loopback listener. That only works when the browser is on the same machine as `fj`: sign in from an iPad or against a remote server and the redirect to `127.0.0.1:47017/callback` can't reach the listener, so sign-in can't complete. (Confirmed live: an iPad redirect failed to load but the `code` was still copyable from Safari's address bar.) This adds `--manual` (alias `--no-browser`) to keep the whole Authorization Code + PKCE flow and change only how the code is obtained: instead of binding a listener, fj prints the authorize URL, the user signs in on any device, and pastes the redirected URL (or the bare code) back. No platform or Paragon changes: `fj-cli` is already registered with the loopback redirect, so manual mode pins the first registered URI (`http://127.0.0.1:47017/callback`) for the exact-match token exchange. This is the interim path; OAuth device flow (separate work) is the durable replacement. ## What changed - `--manual` / `--no-browser` on `fj auth login` (Fjord mode only). `login_fjord_oidc` splits into loopback vs manual code acquisition sharing PKCE, the token exchange, and persistence. - Manual mode parses the paste (`src/fjord/oidc.rs::parse_pasted_callback`): a full redirected URL, just its `code=…&state=…` query, or a bare code. The returned `state` is CSRF-checked when present (`PastedCode::check_state`); a bare code has no state to check and relies on the PKCE verifier (held only by this process) plus the code's single use and short TTL. - Terminal copy for manual sign-in: numbered steps, and it says up front the browser will show a can't-connect error on the `127.0.0.1` page, which is expected. - Branded the loopback callback page (`write_response` → `render_callback_page`), previously a bare `<h1>fj</h1>`. Self-contained HTML: inline styles and the inlined Fjord mark (the page is served off the loopback with no network, so no external asset would load), light/dark aware via `prefers-color-scheme`, palette mirrored from the fjord-platform design tokens. Success / failure / state-mismatch / not-found variants. Manual mode never reaches this page (it binds no listener); it's for the loopback flow. ## Not covered / notes - Manual mode still targets the same `/oauth/authorize` + `/oauth/token` front door; nothing server-side changes. - The branded page can't show the account email (the code hasn't been exchanged yet at render time); the terminal success line already prints it. ## Checks - `cargo fmt --all -- --check` - `cargo clippy --all-targets -- -D warnings` (clean) - `cargo test` (666 pass, 2 ignored; new unit tests cover URL/query/bare-code parsing, empty/error/codeless rejection, the state CSRF-check, a mocked manual exchange end-to-end, and branded-page invariants)
auth: manual/no-browser paste mode for Fjord OIDC login + branded callback page (#224)
All checks were successful
Forseti review / forseti review (advisory) (pull_request_target) Successful in 57s
ci / check (pull_request) Successful in 10m38s
ci / coverage (pull_request) Successful in 2m5s
ci / live-e2e (pull_request) Successful in 2m7s
f5adddd6c1
Add `fj auth login --fjord --manual` (alias `--no-browser`) so a user whose
browser is on a different device than fj (a remote server, an iPad) can complete
SSO/OIDC sign-in. The default loopback capture is same-device only; manual mode
prints the authorize URL, lets the user sign in anywhere, and reads back the
redirected 127.0.0.1 URL (or bare code) they paste even though that page fails
to load on the other device.

The whole Authorization Code + PKCE flow is unchanged except how the code is
obtained: manual mode binds no listener and pins the first registered redirect
URI (http://127.0.0.1:47017/callback) for the exact-match exchange. The returned
state is CSRF-checked when the paste carries one; a bare code relies on the PKCE
verifier (held only by this process) plus the code's single use and short TTL.
No platform/Paragon changes: fj-cli is already a registered PKCE client.

Also brand the loopback callback page (previously bare) to the Fjord premium
bar: self-contained HTML with inline styles and the inlined Fjord mark (served
off the loopback with no network, so no external asset), light/dark aware, with
success / failure / state-mismatch variants.

Tests: URL/query/bare-code parsing, empty/error/codeless rejection, state
CSRF-check, a mocked manual exchange end-to-end, and branded-page invariants
(self-contained, theme-aware, no leftover placeholders).

Interim; OAuth device flow (separate work) is the durable replacement.

Forseti review

No blocking findings from the lead reviewer.

No inline findings.

  • PR: rasterstate/fj#225
  • Head SHA: f5adddd6c1b9
  • Review job: sha256:e569f57a3d955968a8e70cfbc231c30ebec597e99d76f24e215054b06327c7e4
  • Provider pair: openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001
  • Blocking findings: 0
  • Inline findings: 0
  • Token source: GITHUB_TOKEN
  • Runner: 1405b8a0e06d
  • Run: https://rasterhub.com/rasterstate/fj/actions/runs/374
<!-- forseti:review {"version":2,"repo":"rasterstate/fj","pr":225,"head_sha":"f5adddd6c1b9cf34763ecbb317629fecc134b818","provider_pair":"openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001","policy_version":"stub-policy-v1","prompt_version":"prompt-v2","context_fingerprint":"fnv64:6700206495daae0d","review_job_key":"sha256:e569f57a3d955968a8e70cfbc231c30ebec597e99d76f24e215054b06327c7e4","base_sha":"1251c71b59fee3e2d2919d772e20c3ec8a2dfefb","role":"summary","status":"current"} --> ## Forseti review No blocking findings from the lead reviewer. _No inline findings._ - PR: `rasterstate/fj#225` - Head SHA: `f5adddd6c1b9` - Review job: `sha256:e569f57a3d955968a8e70cfbc231c30ebec597e99d76f24e215054b06327c7e4` - Provider pair: `openai:gpt-5.5+anthropic:claude-haiku-4-5-20251001` - Blocking findings: `0` - Inline findings: `0` - Token source: `GITHUB_TOKEN` - Runner: `1405b8a0e06d` - Run: https://rasterhub.com/rasterstate/fj/actions/runs/374
stephen deleted branch feat/224-manual-fjord-login 2026-07-21 19:39:49 +00:00
stephen referenced this pull request from a commit 2026-07-22 15:40:40 +00:00
Sign in to join this conversation.
No description provided.