Skip to content

fix(oauth): drop partitioned: true and use sameSite: "lax" cookies#322

Open
milehighideas wants to merge 1 commit into
get-convex:mainfrom
milehighideas:fix/mobile-oauth-cookies
Open

fix(oauth): drop partitioned: true and use sameSite: "lax" cookies#322
milehighideas wants to merge 1 commit into
get-convex:mainfrom
milehighideas:fix/mobile-oauth-cookies

Conversation

@milehighideas

@milehighideas milehighideas commented Apr 8, 2026

Copy link
Copy Markdown

Summary

Fixes #218.

The cookies Convex Auth sets during OAuth flows (PKCE code_verifier, state, nonce, redirectToParam) are dropped by iOS ASWebAuthenticationSession on the return leg from the OAuth provider, because of the sameSite: "none" + partitioned: true combo in SHARED_COOKIE_OPTIONS. Switching to sameSite: "lax" and dropping partitioned restores the flow on native without breaking web.

Repro

  1. Expo / React Native app using @convex-dev/auth with the Google provider.
  2. Open the OAuth flow via expo-web-browser:
    const { redirect } = await signIn('google', { redirectTo });
    const result = await WebBrowser.openAuthSessionAsync(
      redirect.toString(),
      redirectTo,
      { preferEphemeralSession: true },
    );
  3. Sign in with Google.

Observed before this PR:

  • checkOAuthBodyError from processAuthorizationCodeOpenIDResponse during the Google token exchange — the PKCE cookie is missing on the callback leg, so handleOAuth sends the "decoy" verifier and Google rejects the exchange.
  • Uncaught Error: Invalid state from userOAuthImpl for providers that rely on the state cookie (another user reports this on GitHub OAuth in the linked issue).
  • On providers where the token exchange does succeed, the final redirect uses SITE_URL instead of the mobile scheme because the redirectToParam cookie is also missing — the user lands on the web homepage instead of being deep-linked back into the app.

Root cause

ASWebAuthenticationSession (especially with preferEphemeralSession: 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 the partitioned cookie 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: true is 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

  • Native (Expo / React Native): Fixes the issue.
  • Standard web (Next.js, SPA, SSR): No regression. The OAuth callback is a top-level navigation, which sameSite: "lax" permits; partitioned: true was never required here.
  • Cross-domain iframe embedding: If anyone is embedding Convex Auth UI inside another site's iframe and relying on SameSite=None; Partitioned for 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

  • Verified fix locally in a production Expo app against a real Google OAuth flow (staging Convex deployment) — sign-in now completes end-to-end, the mobile deep link fires correctly, and the second signIn('google', { code }) call succeeds.
  • Repo CI (upstream tests) — pending PR checks.

Notes

Happy to iterate on the approach if maintainers would prefer:

  1. Making SHARED_COOKIE_OPTIONS user-configurable via the convexAuth() config (opt-in behavior change), or
  2. Providing a separate set of cookie options specifically for the OAuth flow while keeping the main cookies as-is.

But 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

  • Bug Fixes
    • Enhanced OAuth authentication security by adjusting cookie handling during the sign-in process for improved reliability and cross-site request protection.

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.
@vercel

vercel Bot commented Apr 8, 2026

Copy link
Copy Markdown

@milehighideas is attempting to deploy a commit to the Convex Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Modified Convex Auth OAuth-flow cookie configuration by changing sameSite from "none" to "lax" and removing the partitioned: true attribute, affecting how PKCE credentials (code_verifier, state, nonce, redirectTo) are handled during OAuth provider callback redirects.

Changes

Cohort / File(s) Summary
OAuth Cookie Configuration
src/server/cookies.ts
Updated SHARED_COOKIE_OPTIONS by changing sameSite from "none" to "lax" and removing partitioned: true attribute, altering cookie behavior during OAuth callback round-trips.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A cookie tweak from none to lax so fine,
Partitions gone, security will shine,
OAuth flows dance along the line,
Safe redirects in this design! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: removing partitioned: true and changing sameSite from "none" to "lax" in OAuth cookies, which directly addresses the root cause of the issue.
Linked Issues check ✅ Passed The code changes directly address the root cause identified in issue #218: OAuth cookies being dropped during iOS ASWebAuthenticationSession redirects, implementing the required fix to restore PKCE/state cookie persistence.
Out of Scope Changes check ✅ Passed All changes are scoped to SHARED_COOKIE_OPTIONS in src/server/cookies.ts and directly support the OAuth cookie fix objective with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c84ea299-d98a-48dd-be73-080dc62bbcb7

📥 Commits

Reviewing files that changed from the base of the PR and between 91b46c3 and 21ec89b.

📒 Files selected for processing (1)
  • src/server/cookies.ts

Comment thread src/server/cookies.ts
Comment on lines 19 to 24
export const SHARED_COOKIE_OPTIONS = {
httpOnly: true,
sameSite: "none" as const,
sameSite: "lax" as const,
secure: true,
path: "/",
partitioned: true,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 -40

Repository: 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 -30

Repository: 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.ts

Repository: 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:


🏁 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 -50

Repository: 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:

  1. Change sameSite to "none" and ensure secure: true is set (which it is)
  2. Make cookie options configurable per provider
  3. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Google issue with react-native after redirect

1 participant