Skip to content

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

Closed
tran-christian wants to merge 10 commits into
mainfrom
fix/auth-callback-session
Closed

tran-christian wants to merge 10 commits into
mainfrom
fix/auth-callback-session

Conversation

@tran-christian

Copy link
Copy Markdown
Contributor

Root Cause

Supabase auth logs confirmed: every Google OAuth and magic link attempt returned /token 200 (Supabase-side login succeeded) but users were never signed in on the site. The session tokens were generated but never delivered to the browser.

The bug: setAll() inside the createServerClient callback wrote cookies to Next.js's cookieStore, but Next.js does not automatically propagate cookies() mutations into a NextResponse.redirect() response object. The Set-Cookie headers were simply missing from the redirect.

Changes

app/auth/callback/route.ts

  • Cookie fix (primary): collect cookies in pendingCookies[] during setAll(), then explicitly stamp them onto the NextResponse.redirect() before returning
  • x-forwarded-host (Vercel): on Vercel the request origin is the internal load-balanced host — use x-forwarded-host in production so the redirect URL is correct
  • next param validation: only allow relative paths to prevent open-redirect

src/components/LoginDrawer.tsx

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

Verification

Supabase auth logs (last 24h): 6+ Google OAuth cycles all showing /token 200 login — Supabase is working, the cookie delivery is the only gap. All 19 existing tests pass.

Notes

The 500: Multiple accounts with the same email address error in the logs is a separate issue — logging into GitHub with the same email as a Google account. Fix: Authentication → Providers → Link identities by email in the Supabase dashboard.

Without a root middleware.ts, Supabase SSR never refreshes the session
cookie on the server. This causes auth state to silently break after the
JWT expires (~1 hour) on server-rendered routes and the auth callback.

The updateSession helper already existed in src/lib/supabase/middleware.ts
but was never wired up as a Next.js middleware.

Matcher excludes static assets and images to avoid unnecessary overhead.
Next.js 16.0.0 deprecated middleware.ts in favor of proxy.ts, with the
exported function renamed from `middleware` to `proxy`. The old file was
being silently ignored on Vercel, which is why session refresh never ran
and auth broke.

Ref: https://nextjs.org/docs/messages/middleware-to-proxy
Matches Next.js 16 naming convention where the root proxy.ts now imports
from '@/lib/supabase/proxy' instead of '@/lib/supabase/middleware'.
Updates index.ts barrel export accordingly.
Replaces supabase.auth.getUser() with supabase.auth.getClaims() in the
session refresh proxy. getUser() makes a network round-trip on every
request and can cause random sign-outs in SSR; getClaims() reads JWT
claims locally and is the correct method for the proxy layer per
Supabase SSR docs.

Also adds .env*.local to .gitignore.
Exclude /auth/callback from SSR middleware matcher so session-refresh
logic cannot clear the code_verifier cookie before exchangeCodeForSession
runs. Revert getClaims() to conditional getUser() — only fires when a
session token cookie is present, avoiding an unconditional auth-service
round-trip for anonymous traffic. Remove non-standard data?.session guard
in callback route. Narrow error-path cookie cleanup to only expire the
PKCE code_verifier cookie, preserving valid sessions on transient errors.
Add proxy unit tests covering conditional getUser() behaviour.
@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 5:49am

…xchange

Next.js does not automatically propagate cookies() mutations into a
NextResponse.redirect() response, so the browser never received
Set-Cookie headers after exchangeCodeForSession — leaving users
perpetually signed out despite Supabase confirming a successful login.

Fix:
- Collect cookies from setAll() into pendingCookies[] and explicitly
  apply them to the NextResponse.redirect() before returning it
- Handle x-forwarded-host so redirect URLs are correct on Vercel (behind
  a load balancer where `origin` is the internal host, not the public one)
- Validate the `next` param to only allow relative paths
- Await signInWithProvider() in LoginDrawer so errors surface via toast
  instead of being swallowed silently

Root cause confirmed via Supabase auth logs: /token returned 200 (login)
for every Google OAuth attempt, but users were never signed in — the
session tokens were generated but never delivered to the browser.
@tran-christian

Copy link
Copy Markdown
Contributor Author

Superseded by #832 — clean branch off main with all conflicts resolved and security fixes applied.

This branch was successfully deployed

1 active deployment
Preview — 581b03dc 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