fix: keep the PKCE verifier out of OAuth state - #78
Conversation
The PKCE code verifier was sealed into the OAuth `state` parameter and round-tripped through the authorization response URL, while the callback's double-submit cookie check compared the URL `state` against a cookie whose value was that same sealed `state`. Both comparands came from the same inbound request, so possession of a leaked callback URL (`?code=...&state=...`) was enough to recover the verifier and complete the code exchange as the victim, with no access to the initiating browser's cookie.
Store the code verifier only in the HttpOnly cookie (sealed as `{ nonce, codeVerifier }`) and seal the URL `state` as `{ nonce, customState, returnPathname }` with no secret. The callback recovers the verifier from the cookie and binds it to the URL state by matching the shared nonce, so a leaked callback URL alone can no longer complete the exchange.
Original prompt from Linear User
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
Re: Greptile's P1 "Mixed Versions Reject Valid Callbacks" (src/get-authorization-url.ts:50-55) — acknowledged, but I don't think it should change this PR. This is a transient rolling-deploy artifact, not a correctness bug in the new code, and it's inherent to any change of the
So I'm intentionally leaving this as-is. If zero login disruption during rollout is a hard requirement, the right lever is deploy-level (drain / blue-green, or a temporary dual-read window that is explicitly not the vulnerable equality check), which is out of scope for this security fix. Flagging for the human reviewer to decide. |
|
Recovered this branch onto current main and added a regression for legacy callback-state rejection. Old-format in-flight sign-ins must restart after deployment. New sign-ins, custom state, and concurrent flows remain covered. Verification: 174 tests passed, including coverage gates. Lint, typecheck, formatting, and build passed. A fresh independent code review found no blocker. Not merged or released. |
|
❌ Cannot revive Devin session - the session is too old. Please start a new session instead. |
|
Addressed the mixed-version rollout concern in b339f01 by documenting the deployment requirement in the README and PR description. The validation remains fail-closed. Greptile is correct that sign-ins crossing versions can fail. The earlier Devin comment saying old-format callbacks still succeed is no longer accurate: the current implementation explicitly rejects URL state containing Use a coordinated cutover for sign-in and callback handlers, and restart interrupted sign-ins afterward. Existing authenticated sessions are unaffected. Verification: all 174 tests pass, including legacy replay rejection and genuine concurrent callback coverage. |
Summary
The PKCE code verifier was serialized into the OAuth
stateparameter, so it round-tripped through the authorization response URL. This change keeps the verifier in the cookie only and reconstructs it on callback, so it no longer travels in the URL.Please review before merging.
Deployment compatibility
This security fix intentionally rejects the previous shared state/cookie format. Old-format callbacks reaching updated instances and new-format callbacks reaching old instances both fail during a mixed-version rollout. Accepting legacy URL state would retain the callback-replay vulnerability.
Deploy sign-in and callback handlers together with a coordinated cutover. In-flight sign-ins using the old format must restart afterward. Existing authenticated sessions are unaffected. The README now documents this upgrade requirement.