Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: prevent rolling session from re-creating a deleted session on concurrent logout (#2335)#2530

Merged
Piyush-85 merged 2 commits into
auth0:mainfrom
sleitor:fix/rolling-session-race-condition
Apr 7, 2026
Merged

fix: prevent rolling session from re-creating a deleted session on concurrent logout (#2335)#2530
Piyush-85 merged 2 commits into
auth0:mainfrom
sleitor:fix/rolling-session-race-condition

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented Feb 20, 2026

Problem

Fixes #2335.

When a user logs out while concurrent in-flight requests are being processed, the middleware was writing a fresh Set-Cookie header 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() called store.set(sessionId, session) unconditionally. If the session had already been deleted by a concurrent logout (via store.delete()) the write would re-insert it, reviving the session.

2. Middleware – rolling update fired even when rolling: false

The else branch in AuthClient.handler() that touches sessions on every non-auth request was reading and re-writing the session even when the session store was configured with rolling: 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 session

Before 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 no Set-Cookie header 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.

// src/server/session/stateful-session-store.ts
if (existingSessionId !== null) {
  const existingSession = await this.store.get(existingSessionId);
  if (!existingSession) {
    return; // session was deleted (logout) – do not re-create it
  }
}

Fix 2 – Middleware: honour rolling: false

A new isRolling getter is exposed on AbstractSessionStore. The middleware now gates the session-touch block behind this.sessionStore.isRolling, eliminating unnecessary reads from the session store (and the associated Set-Cookie responses) when rolling sessions are disabled.

// src/server/auth-client.ts
if (this.sessionStore.isRolling) {
  const session = await this.sessionStore.get(req.cookies);
  if (session) {
    await this.sessionStore.set(req.cookies, res.cookies, { ...session });
    addCacheControlHeadersForSession(res);
  }
}

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:

    • A session deleted in the store (simulating a concurrent logout) is not re-created.
    • A session that still exists in the store is rolled normally.
    • A new-login session (isNew = true) bypasses the guard (session fixation prevention takes priority).
  • auth-client.test.ts – one new case verifying that no Set-Cookie header is emitted when rolling: false.

@sleitor sleitor requested a review from a team as a code owner February 20, 2026 00:00
…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
@sleitor sleitor force-pushed the fix/rolling-session-race-condition branch from f5134c3 to d5511ba Compare March 24, 2026 17:04
@sleitor
Copy link
Copy Markdown
Contributor Author

sleitor commented Mar 24, 2026

Rebased on latest main (through Fix/gh 2440 / v4.16.0) — the conflict in with-page-auth-required.ts was from an unrelated PR that got merged upstream. Our rolling-session fix is unchanged; only the rebase was needed to clear the merge conflict.

@Piyush-85
Copy link
Copy Markdown
Contributor

Piyush-85 commented Apr 7, 2026

Thanks for this contribution @sleitor.
The fix works perfectly vast majority of real-world cases since logout typically completes well before an in-flight request finishes.
The get + set guard reduces the race window but doesn't close it. Logout can still land between the two calls:

store.get("ses_abc")   → exists ✓
  ← logout deletes the row here
store.set("ses_abc")   → upsert re-creates it  

For a 100% fix, adding an optional update?() to SessionDataStore that does a conditional write — UPDATE WHERE id = $1, not an upsert.
The check and the write become one atomic DB operation so there is no gap for logout to slip through. Stores that don't implement it fall back to this get + set behaviour unchanged, fully backward-compatible.

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.50%. Comparing base (f6bfef2) to head (4a78ea8).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Piyush-85 Piyush-85 merged commit e2e832e into auth0:main Apr 7, 2026
8 of 9 checks passed
@sleitor sleitor deleted the fix/rolling-session-race-condition branch April 7, 2026 22:06
@Piyush-85 Piyush-85 mentioned this pull request Apr 9, 2026
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.

Rolling session race condition

3 participants