fix: prevent rolling session from re-creating a deleted session on concurrent logout (#2335)#2530
Conversation
…ncurrent logout (auth0#2335) When a user logs out while concurrent in-flight requests are still being processed, each of those requests' middleware was writing a fresh Set-Cookie header to roll the session. If any of those responses arrived at the browser *after* the logout response the session cookie was silently restored, leaving the user apparently still authenticated. Two complementary fixes address the root cause: 1. StatefulSessionStore – race-condition guard in set() Before persisting a rolling-session update the store is queried with the session ID that was found in the incoming request cookie. If the session no longer exists (because a concurrent logout already called store.delete()) the write is skipped and no Set-Cookie header is emitted, so the browser never gets a revived session cookie. New sessions (isNew=true) and sessions without an existing cookie bypass the guard so that login and first-visit flows are not affected. 2. Middleware (auth-client.ts) – honour the rolling configuration The else-branch that touches sessions on every non-auth request was unconditionally reading and re-writing the session even when rolling: false. It now checks sessionStore.isRolling first, fixing the TODO that has existed in that branch since the feature was written. A new isRolling getter is added to AbstractSessionStore to expose this. Tests added: - StatefulSessionStore: three new cases under 'rolling session race condition' verifying that a deleted session is not re-created, that a live session is still rolled normally, and that the guard is skipped for brand-new (isNew) sessions. - AuthClient middleware: one new case verifying that no Set-Cookie header is emitted when rolling: false. Fixes auth0#2335
f5134c3 to
d5511ba
Compare
|
Rebased on latest |
|
Thanks for this contribution @sleitor. For a 100% fix, adding an optional |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2530 +/- ##
==========================================
+ Coverage 90.48% 90.50% +0.01%
==========================================
Files 53 53
Lines 6687 6699 +12
Branches 1405 1411 +6
==========================================
+ Hits 6051 6063 +12
Misses 624 624
Partials 12 12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Problem
Fixes #2335.
When a user logs out while concurrent in-flight requests are being processed, the middleware was writing a fresh
Set-Cookieheader to roll the session in every response. If any of those responses arrived at the browser after the logout response the session cookie was silently restored, leaving the user apparently still authenticated.Root Causes
Two separate issues contribute to the bug:
1. StatefulSessionStore – no existence check before rolling write
StatefulSessionStore.set()calledstore.set(sessionId, session)unconditionally. If the session had already been deleted by a concurrent logout (viastore.delete()) the write would re-insert it, reviving the session.2. Middleware – rolling update fired even when
rolling: falseThe
elsebranch inAuthClient.handler()that touches sessions on every non-auth request was reading and re-writing the session even when the session store was configured withrolling: false. This is the TODO that has existed in that branch since the feature was written.Fixes
Fix 1 –
StatefulSessionStore.set(): guard against reviving a deleted sessionBefore persisting a rolling-session update the store is now queried with the session ID found in the incoming request cookie. If the session no longer exists (a concurrent logout already called
store.delete()) the write is skipped and noSet-Cookieheader is emitted. The browser never receives a revived session cookie.The guard only applies to rolling updates (sessions with an existing cookie,
isNew = false). Brand-new sessions (fresh login, first visit) bypass the check so those flows remain unaffected.Fix 2 – Middleware: honour
rolling: falseA new
isRollinggetter is exposed onAbstractSessionStore. The middleware now gates the session-touch block behindthis.sessionStore.isRolling, eliminating unnecessary reads from the session store (and the associatedSet-Cookieresponses) when rolling sessions are disabled.Limitations
The stateless (cookie-only) session store cannot be fixed with a pure server-side guard because all session state lives in the encrypted cookie itself — there is no server-side record to check for revocation. For stateless sessions the recommended mitigation remains to scope the middleware matcher to
/auth/*routes only, or to migrate to a stateful session store.Tests
stateful-session-store.test.ts– three new cases under rolling session race condition:isNew = true) bypasses the guard (session fixation prevention takes priority).auth-client.test.ts– one new case verifying that noSet-Cookieheader is emitted whenrolling: false.