fix(oauth): drop partitioned: true and use sameSite: "lax" cookies#322
fix(oauth): drop partitioned: true and use sameSite: "lax" cookies#322milehighideas wants to merge 1 commit into
partitioned: true and use sameSite: "lax" cookies#322Conversation
iOS `ASWebAuthenticationSession` — particularly with
`preferEphemeralSession: true`, which is the recommended setting for OAuth
flows in Expo apps — does not carry the PKCE verifier / state / nonce /
redirectTo cookies back to `/api/auth/callback/*` after the provider
redirects the browser. The `sameSite: "none" + partitioned: true` combo is
the trigger: WebKit treats the partitioned cookie as belonging to a
different partition when the top-level origin flips to the OAuth provider
and back, so the cookies are dropped on the return leg.
Symptoms users hit:
- Google: `checkOAuthBodyError` thrown from
`processAuthorizationCodeOpenIDResponse` during the token exchange
(the "decoy" PKCE verifier is sent because the cookie is missing).
- GitHub/others: `Invalid state` thrown from `userOAuthImpl` because
the state cookie is missing.
- `redirectTo` falls through to `SITE_URL` in the callback handler
because the `redirectToParam` cookie is missing, sending mobile
users to the web homepage instead of back into the app.
Switching to `sameSite: "lax"` (and dropping `partitioned`) still works
for every flow Convex Auth actually performs: the OAuth callback is a
top-level navigation on the same convex.site origin, which `lax`
allows. `lax` is the default for modern browsers and is the safe choice
for OAuth state cookies.
Fixes get-convex#218.
|
@milehighideas is attempting to deploy a commit to the Convex Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughModified Convex Auth OAuth-flow cookie configuration by changing Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/server/cookies.ts`:
- Around line 19-24: The SHARED_COOKIE_OPTIONS currently sets sameSite: "lax",
which breaks POST-based OAuth callbacks (e.g., Apple Sign In); update the
SHARED_COOKIE_OPTIONS constant so sameSite is "none" and keep secure: true to
ensure cookies are sent on cross-site POSTs, or alternately make the cookie
options configurable per provider (e.g., allow overriding SHARED_COOKIE_OPTIONS
for Apple) or force providers to use query-based response_mode—locate
SHARED_COOKIE_OPTIONS in src/server/cookies.ts and modify its sameSite value or
add provider-specific configuration points.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| export const SHARED_COOKIE_OPTIONS = { | ||
| httpOnly: true, | ||
| sameSite: "none" as const, | ||
| sameSite: "lax" as const, | ||
| secure: true, | ||
| path: "/", | ||
| partitioned: true, | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for Apple provider configuration and form_post handling to assess impact
echo "=== Searching for Apple OAuth provider references ==="
rg -n -C3 'apple|Apple' --type=ts -g '!node_modules/**'
echo ""
echo "=== Searching for form_post or response_mode configuration ==="
rg -n -C3 'form_post|response_mode' --type=ts -g '!node_modules/**'
echo ""
echo "=== Checking how callback handles POST requests ==="
rg -n -C5 'application/x-www-form-urlencoded' --type=ts -g '!node_modules/**'Repository: get-convex/convex-auth
Length of output: 12426
🏁 Script executed:
# Check how cookies are actually used in the callback and OAuth flow
echo "=== Examining cookie usage in implementation ==="
head -n 350 src/server/implementation/index.ts | tail -n 100
echo ""
echo "=== Search for cookie reading/setting in callback context ==="
rg -n 'cookies|Cookie' --type=ts src/server/implementation/index.ts -A2 -B2
echo ""
echo "=== Check oauth callback file for cookie handling ==="
rg -n 'cookies|state|code_verifier|nonce' --type=ts src/server/oauth/callback.ts | head -40Repository: get-convex/convex-auth
Length of output: 6617
🏁 Script executed:
# Check `@auth/core` Apple provider documentation and behavior
echo "=== Check if Apple provider uses form_post ==="
find . -type f -name "*.ts" -o -name "*.json" | xargs grep -l "@auth/core\|auth/core" | head -5
echo ""
echo "=== Search for OAuth state/nonce/code_verifier handling ==="
rg -n 'state|code_verifier|nonce' --type=ts src/server/ | head -30Repository: get-convex/convex-auth
Length of output: 2724
🏁 Script executed:
# Understand cookie storage and retrieval mechanism
echo "=== Check how cookies are handled in the auth flow ==="
rg -n 'getCookie|setCookie' --type=ts src/server/ -B2 -A2
echo ""
echo "=== Look at cookies.ts usage ==="
rg -n 'SHARED_COOKIE_OPTIONS' --type=ts src/server/Repository: get-convex/convex-auth
Length of output: 1658
🏁 Script executed:
# Check the Apple provider test configuration to understand the actual flow
echo "=== Check Apple configuration in test setup ==="
rg -n -A20 "Apple\({" test/convex/auth.ts
echo ""
echo "=== Check if there are any tests for Apple callback ==="
fd -t f -e test.ts -e spec.ts | xargs rg -l "apple|Apple" 2>/dev/null | head -5
echo ""
echo "=== Check authorizationUrl.ts to understand how providers are configured ==="
head -n 100 src/server/oauth/authorizationUrl.tsRepository: get-convex/convex-auth
Length of output: 4171
🌐 Web query:
@auth/core Apple provider response_mode form_post
💡 Result:
For Sign in with Apple, when you request name and/or email, Apple returns the authorization response by POSTing a form (application/x-www-form-urlencoded) to your redirect_uri, so you must set response_mode=form_post (instead of the usual query redirect). [1][2]
In @auth/core (Auth.js), configure the Apple provider like this:
import Apple from "@auth/core/providers/apple"
providers: [
Apple({
clientId: process.env.AUTH_APPLE_ID,
clientSecret: process.env.AUTH_APPLE_SECRET,
authorization: {
params: {
scope: "name email",
response_mode: "form_post",
},
},
}),
]Also ensure your callback endpoint (e.g. /api/auth/callback/apple) accepts POST requests and can read form-urlencoded bodies, since Apple will form_post the response to it. [1][3]
Sources:
[1] Apple Developer Documentation (Sign in with Apple authorization request) (developer.apple.com)
[2] OpenAuth Apple provider docs (form_post requirement) (openauth.js.org)
[3] Arctic “Apple” provider notes (Apple posts callback) (v1.arcticjs.dev)
Citations:
- 1: https://developer.apple.com/documentation/signinwithapplerestapi/request-an-authorization-to-the-sign-in-with-apple-server.?utm_source=openai
- 2: https://openauth.js.org/docs/provider/apple/?utm_source=openai
- 3: https://v1.arcticjs.dev/providers/apple?utm_source=openai
🏁 Script executed:
# Read the full Apple configuration from test setup more carefully
cat test/convex/auth.ts | grep -A 30 "^import Apple"
echo ""
echo "=== Look for any response_mode or form_post in entire codebase ==="
rg -n 'response_mode|form_post' . --type=ts --type=json -g '!node_modules/**'Repository: get-convex/convex-auth
Length of output: 1310
🏁 Script executed:
# Check the actual Apple provider implementation imported
echo "=== Find where Apple provider comes from ==="
cat test/convex/auth.ts | head -50Repository: get-convex/convex-auth
Length of output: 1861
🏁 Script executed:
# Check default behavior of `@auth/core` Apple provider
echo "=== Check what `@auth/core` Apple provider does by default ==="
cat package.json | grep "@auth/core"
echo ""
echo "=== Look for any Apple-specific response_mode or form_post handling in convex-auth ==="
rg -n -B5 -A5 'Apple' src/server/ --type=ts
echo ""
echo "=== Check if there's integration test for Apple callback ==="
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) -exec grep -l "apple\|Apple" {} \;Repository: get-convex/convex-auth
Length of output: 1956
sameSite: "lax" will break POST-based OAuth callbacks (e.g., Apple Sign In).
Per the sameSite cookie specification, "lax" cookies are not sent on cross-site POST requests. Apple Sign In uses response_mode=form_post by default (standard behavior from @auth/core), which means Apple POSTs the authorization response to your callback endpoint. With sameSite="lax", the PKCE code_verifier, state, and nonce cookies—which are required by handleOAuth to verify the callback—won't be present, causing the callback to fail.
The code in src/server/implementation/index.ts (lines 309–318) explicitly handles POST requests with a comment noting support for providers like Apple, but the cookie policy undermines this.
Consider one of these alternatives:
- Change
sameSiteto"none"and ensuresecure: trueis set (which it is) - Make cookie options configurable per provider
- Investigate whether providers can be forced to use query-based response mode instead
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/server/cookies.ts` around lines 19 - 24, The SHARED_COOKIE_OPTIONS
currently sets sameSite: "lax", which breaks POST-based OAuth callbacks (e.g.,
Apple Sign In); update the SHARED_COOKIE_OPTIONS constant so sameSite is "none"
and keep secure: true to ensure cookies are sent on cross-site POSTs, or
alternately make the cookie options configurable per provider (e.g., allow
overriding SHARED_COOKIE_OPTIONS for Apple) or force providers to use
query-based response_mode—locate SHARED_COOKIE_OPTIONS in src/server/cookies.ts
and modify its sameSite value or add provider-specific configuration points.
Summary
Fixes #218.
The cookies Convex Auth sets during OAuth flows (PKCE
code_verifier,state,nonce,redirectToParam) are dropped by iOSASWebAuthenticationSessionon the return leg from the OAuth provider, because of thesameSite: "none" + partitioned: truecombo inSHARED_COOKIE_OPTIONS. Switching tosameSite: "lax"and droppingpartitionedrestores the flow on native without breaking web.Repro
@convex-dev/authwith the Google provider.expo-web-browser:Observed before this PR:
checkOAuthBodyErrorfromprocessAuthorizationCodeOpenIDResponseduring the Google token exchange — the PKCE cookie is missing on the callback leg, sohandleOAuthsends the"decoy"verifier and Google rejects the exchange.Uncaught Error: Invalid statefromuserOAuthImplfor providers that rely on thestatecookie (another user reports this on GitHub OAuth in the linked issue).SITE_URLinstead of the mobile scheme because theredirectToParamcookie is also missing — the user lands on the web homepage instead of being deep-linked back into the app.Root cause
ASWebAuthenticationSession(especially withpreferEphemeralSession: true, which is the recommended setting for OAuth flows because it forces an account picker) runs the OAuth flow in an isolated Safari browsing context. WebKit treats thepartitionedcookie as belonging to a different partition when the top-level origin flips from<convex-site>→accounts.google.com→<convex-site>, so cookies set on the outbound leg are dropped on the return leg.sameSite: "none"+partitioned: trueis the combination specifically required for iframe-embedded cross-site flows (e.g. a cross-domain auth widget embedded in another site). Convex Auth's OAuth flow is not an iframe flow — it's a full-page top-level navigation from the callback URL.sameSite: "lax"is the correct and safer default for that, and it's what most OAuth state cookie implementations use.Change
One file:
src/server/cookies.ts.export const SHARED_COOKIE_OPTIONS = { httpOnly: true, - sameSite: "none" as const, + sameSite: "lax" as const, secure: true, path: "/", - partitioned: true, };Compatibility
sameSite: "lax"permits;partitioned: truewas never required here.SameSite=None; Partitionedfor that case, they would be affected. This seems extremely unlikely for an OAuth state cookie flow, but maintainers should confirm whether that use case is supported.Test plan
signIn('google', { code })call succeeds.Notes
Happy to iterate on the approach if maintainers would prefer:
SHARED_COOKIE_OPTIONSuser-configurable via theconvexAuth()config (opt-in behavior change), orBut
sameSite: "lax"is the genuinely correct default for OAuth state cookies, and the current defaults are broken on mobile today, so I'd lean toward just landing the change as-is.Summary by CodeRabbit