Skip to content

fix(react-router): scope getAuth to the request to prevent cross-user auth bleed#8890

Closed
jacekradko wants to merge 4 commits into
mainfrom
jacek/rr-als-auth-scope
Closed

fix(react-router): scope getAuth to the request to prevent cross-user auth bleed#8890
jacekradko wants to merge 4 commits into
mainfrom
jacek/rr-als-auth-scope

Conversation

@jacekradko

@jacekradko jacekradko commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

clerkMiddleware stores each request's auth on the React Router context and getAuth reads it back. Apps that return a single shared RouterContextProvider from a custom server or getLoadContext reuse one context across every request, so under concurrency a request can be served another user's auth. I reproduced it end to end (a real RR v7 app with v8_middleware and a custom server sharing one context: ~45% of 1000 concurrent requests got the wrong user) and confirmed @clerk/backend's authenticateRequest is not the cause.

getAuth (and rootAuthLoader) now re-derive the current request's auth from args.request instead of reading a value cached on the context. Identity is a pure function of the request's cookies, so a shared context can never return another user, including across React Router's action-to-loader revalidation (which mints a fresh Request). The middleware stashes only identity-free options (keys, urls) on the context for the re-derivation to reuse, the same way @clerk/nextjs resolves auth. There is no node:async_hooks dependency, so behavior is uniform across Node, Workers, Deno and Bun.

clerkMiddleware also surfaces a context reused across requests: it throws in development and logs once in production, since a shared context can still leak the application's own per-request data.

Worth a look: the re-derivation in authenticateFromRequest, and that getAuth re-runs authenticateRequest per call (networkless once the middleware has warmed the JWKS cache). Verified end to end: re-derivation holds 0/1000 wrong-user on a shared context where the previous cached read bled ~45%.

(Supersedes the earlier AsyncLocalStorage approach on this branch, per review: re-derivation is fully runtime-portable, needs no new dependency, and matches the pattern our other server SDKs already use.)

Summary by CodeRabbit

  • Bug Fixes
    • Fixed potential authentication leakage between users when a React Router context/provider instance is reused across concurrent requests.
    • getAuth() and rootAuthLoader() now resolve auth from the current request, preventing incorrect auth during overlapping middleware execution and action→loader revalidation.
    • Added a one-time warning when context reuse across requests is detected.
  • Tests
    • Added/updated test coverage for auth isolation and shared-context warning behavior.
  • Documentation
    • Updated the patch release note for the fix and the warning behavior.

… auth bleed

clerkMiddleware stored each request's auth on the React Router context and
getAuth read it back. Apps that share one RouterContextProvider across requests
(a custom server or getLoadContext that returns a single instance) reuse one
context for every request, so under concurrency a request could be served
another user's auth. Bind auth to a per-request AsyncLocalStorage scope around
next() (getAuth falls back to the context slot), which also survives React
Router's action-to-loader revalidation, and warn once when a reused context is
detected.
@changeset-bot

changeset-bot Bot commented Jun 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f5a9200

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/react-router Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
swingset Ready Ready Preview, Comment Jun 17, 2026 4:32pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
clerk-js-sandbox Skipped Skipped Jun 17, 2026 4:32pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

clerkMiddleware gains a new exported requestOptionsContext and authenticateFromRequest helper, plus shared-context reuse detection that logs a one-time warning. getAuth and rootAuthLoader are rewritten to call authenticateFromRequest per-request instead of reading cached state from the React Router context, ensuring isolation across concurrent requests.

Changes

Request-scoped auth isolation for React Router

Layer / File(s) Summary
clerkMiddleware: new exported API and shared-context probe
packages/react-router/src/server/clerkMiddleware.ts
Adds imports for logger, exports requestOptionsContext (createContext<AuthenticateRequestOptions | null>), a sharedContextProbe context with a detailed reuse warning message, and the exported authenticateFromRequest helper that reads stored options from args.context and re-calls clerkClient.authenticateRequest.
clerkMiddleware: reuse detection, auth execution, and context storage
packages/react-router/src/server/clerkMiddleware.ts
Pre-await sync probe comparison detects a reused RouterContextProvider and calls logger.warnOnce when detected; constructs an explicit authenticateOptions object; stores identity-free options into requestOptionsContext and auth state into authFnContext and requestStateContext.
getAuth and rootAuthLoader: re-derive from authenticateFromRequest
packages/react-router/src/server/getAuth.ts, packages/react-router/src/server/rootAuthLoader.ts
getAuth removes authFnContext/IsOptIntoMiddleware lookup and calls authenticateFromRequest(args, 'any') then converts via requestState.toAuth. rootAuthLoader similarly calls authenticateFromRequest(args, 'any') to obtain a fresh requestState before passing it to processRootAuthLoader.
Auth isolation and action→loader tests
packages/react-router/src/server/__tests__/clerkMiddleware.authIsolation.test.ts
Interleaved concurrency tests verify getAuth returns the correct per-request user when middleware calls overlap on a shared RouterContextProvider, with variants for per-request providers and action→loader fresh-request scenarios.
Shared-context warning tests
packages/react-router/src/server/__tests__/clerkMiddleware.sharedContextWarning.test.ts
Spies on logger.warnOnce; asserts exactly one warning is emitted on shared-provider reuse and no warning fires for per-request providers.
Updated unit tests and changeset
packages/react-router/src/server/__tests__/getAuth.test.ts, packages/react-router/src/server/__tests__/rootAuthLoader.test.ts, .changeset/rr-request-scoped-auth.md
getAuth tests mocked to assert re-derivation via requestOptionsContext; rootAuthLoader tests gain a makeContext helper. Changeset documents the patch release and request-scoped auth fix.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant clerkMiddleware
  participant sharedContextProbe
  participant clerkClient
  participant getAuth
  participant rootAuthLoader

  rect rgba(70, 130, 180, 0.5)
    Note over clerkMiddleware,sharedContextProbe: Shared-context detection (sync, pre-await)
    Client->>clerkMiddleware: request + RouterContextProvider context
    clerkMiddleware->>sharedContextProbe: read stored request
    alt stored request differs from current request
      clerkMiddleware->>clerkMiddleware: stamp current request
      clerkMiddleware->>clerkMiddleware: logger.warnOnce(shared context warning)
    end
  end

  rect rgba(60, 179, 113, 0.5)
    Note over clerkMiddleware,clerkClient: Per-request authentication
    clerkMiddleware->>clerkClient: authenticateRequest(request, authenticateOptions)
    clerkClient-->>clerkMiddleware: requestState
    clerkMiddleware->>clerkMiddleware: store requestOptionsContext, authFnContext, requestStateContext
  end

  rect rgba(255, 165, 0, 0.5)
    Note over getAuth,clerkClient: Auth resolution in loaders/actions
    getAuth->>clerkMiddleware: authenticateFromRequest(args, 'any')
    clerkMiddleware->>clerkMiddleware: read requestOptionsContext from args.context
    clerkMiddleware->>clerkClient: authenticateRequest(request, storedOptions)
    clerkClient-->>getAuth: requestState → toAuth()
    rootAuthLoader->>clerkMiddleware: authenticateFromRequest(args, 'any')
    clerkMiddleware->>clerkClient: authenticateRequest(request, storedOptions)
    clerkClient-->>rootAuthLoader: fresh requestState → processRootAuthLoader
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • wobsoriano

Poem

🐇 Hop, hop, no more shared state leaks today,
Each request gets its own auth on the way!
The context reused? A warning appears,
Fresh derivation for every new peer.
Safe concurrency, request-scoped and sound —
Per-request auth is what we have found! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: fixing cross-user auth data leakage by scoping authentication to the request level in React Router.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Jun 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8890

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8890

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8890

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8890

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8890

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8890

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8890

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8890

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8890

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8890

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8890

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8890

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8890

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8890

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8890

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8890

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8890

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8890

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8890

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8890

commit: f5a9200

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-17T16:33:14.130Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on f5a9200.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/react-router/src/server/requestAuthStorage.ts (1)

31-35: ⚡ Quick win

Remove any from the exported request auth state type.

Line 33 currently exposes RequestState<any>, which weakens type safety for downstream consumers. Prefer RequestState (default generic) or a concrete bounded generic here.

Suggested change
 export type RequestAuthState = {
   authFn: (options?: PendingSessionOptions) => AuthObject;
-  requestState: RequestState<any>;
+  requestState: RequestState;
   additionalState: AdditionalStateOptions;
 };

As per coding guidelines: “Avoid any type - prefer unknown when type is uncertain” and “No any types without justification in code review.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-router/src/server/requestAuthStorage.ts` around lines 31 - 35,
The requestState field in the RequestAuthState type definition currently uses
RequestState<any>, which weakens type safety. Replace the RequestState<any> with
either RequestState using its default generic parameter or provide a concrete
bounded generic type instead of any to improve type safety and align with coding
guidelines that discourage any types.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/react-router/src/server/requestAuthStorage.ts`:
- Around line 31-35: The requestState field in the RequestAuthState type
definition currently uses RequestState<any>, which weakens type safety. Replace
the RequestState<any> with either RequestState using its default generic
parameter or provide a concrete bounded generic type instead of any to improve
type safety and align with coding guidelines that discourage any types.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 4e64c2eb-b95a-4aae-9c08-13a07cfc4b41

📥 Commits

Reviewing files that changed from the base of the PR and between eeda12c and 059f3fb.

📒 Files selected for processing (6)
  • .changeset/rr-request-scoped-auth.md
  • packages/react-router/src/server/__tests__/clerkMiddleware.requestScope.test.ts
  • packages/react-router/src/server/__tests__/clerkMiddleware.sharedContextWarning.test.ts
  • packages/react-router/src/server/clerkMiddleware.ts
  • packages/react-router/src/server/getAuth.ts
  • packages/react-router/src/server/requestAuthStorage.ts

Comment on lines +9 to +30
/**
* Per-request auth, bound to the async execution context rather than to the
* React Router request `context`.
*
* `clerkMiddleware` also mirrors the auth onto the RR context (see
* `authFnContext`), but that context is only per-request if React Router is the
* one that creates it. Apps that return a single module-scoped
* `RouterContextProvider` from a custom server / `getLoadContext` share one
* context across every request, so concurrent requests overwrite each other's
* auth and `getAuth` can resolve the wrong user (the reported cross-user bleed).
*
* `AsyncLocalStorage` keys by the running async context instead, which React
* Router gives each request regardless of how the context object was
* constructed, so `getAuth` reads the right user even when the context is
* shared. It also survives the action -> loader revalidation hop, where React
* Router mints a fresh `Request` object (so a request-keyed map would miss) but
* the async scope is unchanged.
*
* Note: this module imports `node:async_hooks`, matching `@clerk/nextjs`'s
* middleware storage. On a runtime without async storage, `getAuth` falls back
* to the RR context slot (today's behavior).
*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to the fix per-se, but lets avoid the comment pollution in javascript as we're trying to do in the cloudflare-workers repo. Ill add an AGENTS rule accordingly

(same for the rest of the comments as well)

…yncLocalStorage

Replace the AsyncLocalStorage request scope with stateless re-derivation: getAuth
and rootAuthLoader re-authenticate from args.request using the identity-free
options the middleware stashes on the context, so a shared RouterContextProvider
can never return another user, including across the action-to-loader hop. Drops
the node:async_hooks dependency (uniform across Node/Workers/Deno/Bun) and covers
the rootAuthLoader path. clerkMiddleware throws in dev / warns once in prod when a
context is reused across requests.
Auth is already isolated by per-request re-derivation, so drop the dev throw:
clerkMiddleware now logs once in every environment when it detects a context
reused across requests (escalate to an error in a future major). Also trims
verbose code comments per review feedback.
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.

2 participants