Skip to content

fix(next-auth): don't duplicate the session cookie on internal auth action routes - #13499

Open
OsamaAnsar wants to merge 1 commit into
nextauthjs:mainfrom
OsamaAnsar:fix/handleauth-signout-duplicate-cookie
Open

OsamaAnsar wants to merge 1 commit into
nextauthjs:mainfrom
OsamaAnsar:fix/handleauth-signout-duplicate-cookie

Conversation

@OsamaAnsar

Copy link
Copy Markdown

Summary

handleAuth() (the standard export { auth as middleware } wrapper most Next.js middleware setups use) runs on every middleware-matched request, including the POST to /api/auth/signout itself.

  • packages/next-auth/src/lib/index.ts (handleAuth, previously around lines 249-305, now 249-320 after the fix): for a JWT session, getSession() (called unconditionally near the top of handleAuth) re-signs and re-sets the session cookie with a fresh expiry as a side effect of merely reading it. handleAuth then unconditionally appended that refreshed Set-Cookie onto whatever the actual route later produced (previously around lines 298-302), regardless of which route is being hit.
  • packages/core/src/lib/actions/session.ts:71-79: the JWT branch of the session action, which is what getSession() invokes internally — this is the code that re-signs the cookie on every read.

Net effect: for a POST to /api/auth/signout, the real signout route handler (invoked separately by Next.js once middleware lets the request through) sets its own Max-Age=0 clearing cookie, and handleAuth also appends its own freshly-signed live session cookie on top of that. The response ends up with two conflicting Set-Cookie headers for the same cookie name. Per RFC 6265 §4.1.1 this is undefined behavior — which cookie a browser keeps depends on header order, which differs by hosting runtime (Vercel/Node vs. Cloudflare Workers vs. Netlify) — so sign-out can silently fail to actually clear the session on some hosts.

Fixes #12909.

Fix

handleAuth now skips appending its own session-refresh Set-Cookie when the request itself is hitting one of NextAuth's internal action routes (signin, signout, callback, session, csrf, providers, verify-request, error — reusing the existing actions set already used by the neighboring isSameAuthAction helper). Those routes manage the session cookie themselves as part of handling the action (e.g. signout clears it), so handleAuth appending a redundant/conflicting cookie on top serves no purpose and is actively wrong for signout.

auth / req.auth is still computed exactly as before (the getSession() call and the authorized callback / user middleware logic are untouched) — the fix only guards the final cookie-append step, so it's scoped specifically to the duplicate-cookie case and doesn't change how the session is read or how authorization decisions are made. Ordinary page requests are completely unaffected.

Test plan

Added packages/next-auth/test/middleware-signout-cookie.test.ts with 3 tests:

  1. A POST to /api/auth/signout through handleAuth (via auth(req, event)) results in zero Set-Cookie headers for the session cookie from handleAuth itself.
  2. An ordinary page request still gets the session cookie refreshed exactly once, with a live (non-Max-Age=0) value — proving normal session-refresh behavior is unaffected.
  3. An end-to-end simulation: handleAuth's response merged with the real /api/auth/signout route handler's response (driven directly through @auth/core's Auth(), the way Next.js would merge middleware and route-handler headers) results in exactly one Set-Cookie for the session cookie, and it's the Max-Age=0 clearing one — not a live one.

Ran:

  • pnpm --filter next-auth exec vitest run -c ../utils/vitest.config.ts test/middleware-signout-cookie.test.ts — all 3 pass.
  • pnpm --filter next-auth exec vitest run -c ../utils/vitest.config.ts (full next-auth suite) — all tests pass except a pre-existing, unrelated failure in test/env.test.ts (stale import path requiring the package to be pre-built; reproduces identically on main without this change).
  • pnpm --filter @auth/core exec vitest run -c ../utils/vitest.config.ts (full @auth/core suite, including test/actions/session.test.ts) — all 160 tests pass, confirming normal session-refresh behavior is unaffected.
  • tsc --noEmit and eslint on the changed files — clean (only pre-existing warnings on unrelated lines).

Confirmed the test catches the regression: reverted just the source fix in packages/next-auth/src/lib/index.ts (kept the new test), re-ran — 2 of the 3 new tests failed exactly as expected (one asserting 0 cookies got 1; the merged-response test asserting 1 cookie got 2, i.e. the live + the clearing cookie both present). Restored the fix and re-ran — all 3 pass again.

🤖 Generated with Claude Code

…ction routes

`handleAuth()` (the `export { auth as middleware }` wrapper) runs on every
middleware-matched request, including the POST to `/api/auth/signout`
itself. For a JWT session it unconditionally called `getSession()`, which
re-signs and re-sets the session cookie with a fresh expiry as a side
effect of merely reading it, and then unconditionally appended that
cookie onto whatever the actual route being hit later produced.

For `/api/auth/signout`, the real signout route handler (invoked
separately by Next.js once middleware lets the request through) already
sets its own `Max-Age=0` clearing cookie. With handleAuth also appending
its freshly-signed live cookie, the final response ends up carrying two
conflicting `Set-Cookie` headers for the same cookie name. Per RFC 6265
4.1.1 this is undefined behavior, and which one a given browser keeps
depends on header order, which differs by hosting runtime (Vercel/Node vs.
Cloudflare Workers vs. Netlify) -- so sign-out can silently fail to clear
the session on some hosts.

Fix: skip appending handleAuth's own session-refresh cookie when the
request itself is hitting one of NextAuth's internal action routes
(signout, callback, session, etc. -- reusing the existing `actions` set
already used by `isSameAuthAction`). Those routes manage the session
cookie themselves as part of handling the action. `auth`/`req.auth` is
still computed exactly as before for the `authorized` callback and
user middleware, so this only changes whether the redundant/conflicting
cookie gets appended, not how the session is read. Normal page requests
are unaffected.

Fixes nextauthjs#12909.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

@OsamaAnsar is attempting to deploy a commit to the authjs Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
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.

During logout, both a blank and non-blank __Secure-authjs.session-token Set-Cookie header are sent by next-auth

1 participant