feat(api+client): OAuth callback → bearer token handoff for native (#215) #249
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!249
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "215-oauth-native-bearer"
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?
Closes #215.
Summary
Adds a three-step OAuth sign-in flow for native clients (Android, Flatpak) that can't hold the existing cookie-based session across the system-browser handoff. The web cookie flow is preserved unchanged — the callback routes by "state hashes to a row" vs. "no row, fall through to cookies".
Route table
/api/auth/oauth/native/initoauth_initsrow; returns{ initId, providerStartUrl, deepLinkOnComplete, expiresAt }./api/auth/oauth/callback/{provider}statehashes to an init row, runs the native completion variant (attach completion-token hash, 302 tocarol://auth/oauth/complete). Otherwise keeps the cookie path unchanged./api/auth/tokengrantTypegains"authorization_code"({ initId, code }); reusesmintTokenPair./api/auth/providersThreat model (every cookie-flow defence preserved 1:1)
consumed_at.providerat init time; the callback compares the URL provider param. OIDC instances also check?iss=when present.markConsumedis a single transition guarded by a "still null" predicate. A replayed(initId, code)returns 401invalid_authorization_code.carol://scheme can only race the legitimate exchange on the network round-trip after seeing the URL.Test plan
state_hash(not raw state), nonce, PKCE verifiercarol://auth/oauth/complete?init=…&code=…?error=oauth_expired?error=oauth_already_completedemail_in_useflows back through the deep link (per ADR-0015 — no auto-merge by email)POST /api/auth/token { grantType: "authorization_code" }happy path mintscat_*+crt_*invalid_authorization_codeGET /api/auth/providersreturns the configured provider listrunOauthNativeSignInorchestrates init → openURL → deep-link → exchange with mocked callbackspnpm -F @carol/api typecheck / lint / test / openapi:check / openapi:coveragegreenpnpm -F @carol/api-client typecheck / lint / checkgreenpnpm -F @carol/client typecheck / lint / test / export:webgreenSee also
🤖 Generated with Claude Code
Adds a three-step OAuth sign-in flow for native clients that can't hold the existing cookie-based session across the system-browser handoff: 1. POST /api/auth/oauth/native/init mints an `oauth_inits` row carrying state hash + PKCE verifier + (OIDC) nonce, returns `{ initId, providerStartUrl, deepLinkOnComplete, expiresAt }`. 2. The native client opens `providerStartUrl` in the system browser via `expo-linking.openURL`. The user authenticates, the IdP redirects to `/api/auth/oauth/callback/{provider}`. 3. The callback hashes the received state, finds the init row, runs the existing linking decision tree, attaches a one-time completion-token hash to the row, and 302s to `carol://auth/oauth/complete?init=…&code=…`. 4. The native app catches the deep link via `expo-linking.addEventListener`, POSTs `{ grantType: "authorization_code", initId, code }` to /api/auth/token, and stashes the returned bearer pair in SecureStore. State, PKCE, nonce, and RFC 9207 issuer checks carry over 1:1 from the cookie flow; the init row's `state_hash` UNIQUE constraint replaces the cookie-equality check. Replay is blocked by a single-shot `consumed_at` transition; rows expire after 5 minutes. The web cookie flow is preserved unchanged — the callback routes by "state hashes to a row" vs. "no row, fall through to cookies". Also adds GET /api/auth/providers (unauthenticated provider list) for the login screen's "Sign in with <Provider>" buttons. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.