Skip to content

fix(auth): stamp session cookies onto redirect response after OAuth/magic link exchange - #832

Merged
tran-christian merged 1 commit into
mainfrom
fix/auth-callback-cookies
Apr 16, 2026
Merged

tran-christian merged 1 commit into
mainfrom
fix/auth-callback-cookies

Conversation

@tran-christian

Copy link
Copy Markdown
Contributor

Root Cause

Confirmed via Supabase auth logs: /token 200 login fired on every Google OAuth and magic-link attempt — Supabase was succeeding. But users were never signed in.

Next.js does not automatically propagate cookies() mutations into a NextResponse.redirect() response. setAll() was writing to the internal cookieStore, but the Set-Cookie headers never appeared on the outgoing redirect response. The browser never received the session.

Changes

app/auth/callback/route.ts

  • Primary fix: buffer cookies in pendingCookies[] during setAll(), then explicitly stamp each onto the NextResponse.redirect() before returning
  • Security: reject //host double-slash bypass in next param open-redirect guard (e.g. //evil.com starts with / but is not a relative path)
  • Security: dropped x-forwarded-host redirect-base derivation — using untrusted request headers for the redirect destination is an open-redirect vector; new URL(request.url).origin is correct on Vercel
  • DX: log exchangeCodeForSession errors for production debugging
  • Perf: move cookieStore await after the oauthError early-return to avoid unnecessary async work
  • Simplification: replace .filter().forEach() with for...of

src/components/LoginDrawer.tsx

  • await signInWithProvider() so errors surface via the existing notifyError toast instead of being silently swallowed

Verification

  • Supabase auth logs (24h): 6+ Google OAuth cycles all showing /token 200 login for the account — Supabase was working, cookie delivery was the only gap
  • All 19 existing tests pass
  • Branch is clean off main (no merge conflicts)

Separate issue (not in this PR)

The 500: Multiple accounts with the same email address log entry is from signing into GitHub with the same email already registered via Google. Fix in Supabase dashboard → Authentication → Providers → Link identities by email.

…xchange

Next.js does not propagate cookies() mutations into NextResponse.redirect()
automatically — the Set-Cookie headers were missing, so the browser never
received session cookies despite Supabase confirming a successful login.

Root cause confirmed via Supabase auth logs: /token returned 200 (login)
on every Google OAuth and magic-link attempt, but users remained signed out.

Changes:
- Collect cookies in pendingCookies[] during setAll(), then explicitly stamp
  them onto the NextResponse.redirect() before returning
- Reject //host double-slash bypass in `next` param open-redirect guard
- Move cookieStore await after the oauthError early-return (avoid unnecessary work)
- Log exchangeCodeForSession errors for production debugging
- Replace filter/forEach with for..of to avoid intermediate array allocation
- Await signInWithProvider() in LoginDrawer so errors surface via notifyError
  toast instead of being silently swallowed
@vercel

vercel Bot commented Apr 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website Ready Ready Preview, Comment Apr 16, 2026 6:00am

@tran-christian
tran-christian merged commit 56bcb2c into main Apr 16, 2026
16 checks passed
@tran-christian
tran-christian deleted the fix/auth-callback-cookies branch April 16, 2026 06:01
tran-christian added a commit that referenced this pull request Apr 17, 2026
…st handling

Replaces the third-attempt manual Set-Cookie stamping with Supabase's
canonical @supabase/ssr App Router pattern and adds x-forwarded-host
handling — the single constraint from the official example that has
never been applied across #830, #832, or the current staged attempt.

Root cause: on Vercel, new URL(request.url).origin can resolve to an
internal load-balancer host rather than the public domain. When the
redirect lands on a different origin than the one @supabase/ssr stamped
Set-Cookie on, the browser appears to have lost the session.

Changes:
- Use createClient from @/lib/supabase/server (already canonical) instead
  of inlining createServerClient with a custom setAll
- Drop pendingCookies[] / response.headers.append('Set-Cookie', ...) and
  the toSetCookieHeader() helper — Next.js propagates cookies().set()
  mutations onto the response on its own
- Prefer x-forwarded-host in production redirects (canonical Supabase
  Next.js App Router example)
- Keep /auth/callback proxy exclusion (from #830) so session-refresh does
  not clear the code_verifier
- Keep a single _fh diagnostic param to confirm which redirect branch ran
tran-christian added a commit that referenced this pull request Apr 18, 2026
NextResponse.redirect(...) returns a standalone response that Next.js
forwards as-is — pending cookie mutations from cookies().set() are NOT
merged onto it. That explains why both the original canonical
cookieStore.set() pattern (Jan-Apr baseline) and the manual
response.cookies.set() / headers.append('Set-Cookie') stamping (#832,
earlier staged) have all failed to deliver session cookies in production.

redirect() from next/navigation instead throws NEXT_REDIRECT which the
framework catches and converts to a redirect response that DOES inherit
the cookie-mutation buffer. This is the pattern Supabase's canonical
/auth/confirm example uses.

Verified on preview: sb-*-auth-token-code-verifier landed on the domain
but sb-*-auth-token never did, matching the production symptom exactly.

Keep x-forwarded-host branch and _fh diagnostic; update doc block.

This branch was successfully deployed

1 active deployment
Preview — bd255d2b Deployed Apr 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant