Real auth: /v1/oauth/{state,exchange,handoff/{id}} with PKCE-S256, S2S guard, atomic getDel#15
Conversation
There was a problem hiding this comment.
rainxchzed has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (18)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…arrow ALLOWED_HOSTS default, document the deep-link contract
… GitHub error whitelist, MessageDigest.isEqual, 401-on-misconfig, no-store on every handoff response, X-Oauth-Service-Token in Sentry scrubber
… in prod, OAUTH_CLEANUP_DISABLED kill switch, drop unused Exposed OAuthEphemeral declaration
95846d7 to
2615ed6
Compare
There was a problem hiding this comment.
rainxchzed has reached the 50-review limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Summary
Server-side half of the GitHub OAuth web flow with PKCE-S256 and a short-lived handoff. App opens browser → GitHub → website callback → backend swaps code for token → backend mints opaque
handoff_id→ app receives id via deep link → app calls backend with id → gets token. Token traverses the network only inside the handoff response body; never appears in a URL, never logged, lives in Postgres for ≤60s, single-use.8 commits. The last two are review fixes from a Backend Architect + Security Engineer pass.
Endpoints
POST /v1/oauth/state{state, code_challenge}. 60s TTL. 60/hr/IP.POST /v1/oauth/exchangegetDel(state), swaps with GitHub, mints handoff. 30/hr/IP.POST /v1/oauth/handoff/{id}DELETE ... RETURNING value— single-use, second call 404s. 60/min/IP.Storage
Postgres-backed via
oauth_ephemeraltable (V16 migration). Single table, two namespaces (state,handoff). 60s TTL enforced in two ways:expires_at > NOW()— expired-but-unswept rows appear absentOAuthCleanupWorkerreaps expired rows every 5min (withOAUTH_CLEANUP_DISABLED=truekill switch)Interface-abstracted (
OAuthEphemeralStore) for a possible future Redis swap.Security baseline compliance
MessageDigest.isEqual(SHA256(verifier), stored_challenge)— constant-time^[A-Za-z0-9_-]{43}$enforced on every value{access_token}body of/handoffresponseDELETE … RETURNING valueis atomic at the SQL row-lock levelTTL = Duration.ofSeconds(60)constantgho_test_secret_token+ rawcode+ verifier, asserts none appear in log captureReview fixes (last two commits)
Security:
getDel(state)at top of /exchange — closes race between two concurrent(state, verifier)requests + collapses the state-burn asymmetry (GitHub's authorization code is single-use upstream, so retry-with-same-state is never legitimate)github_unknown401 service_auth_requiredbody as wrong-secret — anonymous prober can't fingerprint deploy stateMessageDigest.isEqualfor both service-token compare and PKCE compare — no length-side-channelCache-Control: no-storeset as first action on every /handoff response, including 404 pathsX-Oauth-Service-Tokenadded toSentry.beforeSendheader scrubberArchitecture:
validateProductionEnvnow requiresOAUTH_SERVICE_ALLOWED_HOSTS+OAUTH_WEB_CALLBACK_URL— both fail-fast at boot instead of 401-on-every-request at runtimeOAUTH_CLEANUP_DISABLED=trueper-iteration kill switch inOAuthCleanupWorkerOAuthEphemeraldeclaration; replaced with a comment explaining why the table is JDBC-only (footgun removal)Deep-link contract (operator → website agent)
Website emits
githubstore://auth?handoff=<id>&state=<state>. State round-trips: app generated, website stored in its KV, included in deep link, app re-validates locally before consuming handoff. Backend never emits the URL. Earlier draftoauth-done?handoff=<id>is superseded.New env vars
All required under
APP_ENV=production— backend refuses to start without them. OldGITHUB_OAUTH_CLIENT_IDis no longer read.Test plan
curl -X POST -H 'X-Oauth-Service-Token: \$TOKEN' -H 'Host: api.github-store.org' -d '{"state":"x","code_challenge":"y"}' https://api.github-store.org/v1/oauth/state→ 400 invalid_state (proves endpoint up + auth gates work + validation rejects dummies)