fix(react-router): scope getAuth to the request to prevent cross-user auth bleed#8890
fix(react-router): scope getAuth to the request to prevent cross-user auth bleed#8890jacekradko wants to merge 4 commits into
Conversation
… 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 detectedLatest commit: f5a9200 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthrough
ChangesRequest-scoped auth isolation for React Router
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
API Changes Report
Summary
No API Changes DetectedAll packages have stable APIs with no detected changes. Report generated by Break Check Last ran on |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/react-router/src/server/requestAuthStorage.ts (1)
31-35: ⚡ Quick winRemove
anyfrom the exported request auth state type.Line 33 currently exposes
RequestState<any>, which weakens type safety for downstream consumers. PreferRequestState(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
anytype - preferunknownwhen type is uncertain” and “Noanytypes 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
📒 Files selected for processing (6)
.changeset/rr-request-scoped-auth.mdpackages/react-router/src/server/__tests__/clerkMiddleware.requestScope.test.tspackages/react-router/src/server/__tests__/clerkMiddleware.sharedContextWarning.test.tspackages/react-router/src/server/clerkMiddleware.tspackages/react-router/src/server/getAuth.tspackages/react-router/src/server/requestAuthStorage.ts
| /** | ||
| * 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). | ||
| */ |
There was a problem hiding this comment.
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.
clerkMiddleware stores each request's auth on the React Router
contextand getAuth reads it back. Apps that return a single sharedRouterContextProviderfrom a custom server orgetLoadContextreuse 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 withv8_middlewareand a custom server sharing one context: ~45% of 1000 concurrent requests got the wrong user) and confirmed@clerk/backend'sauthenticateRequestis not the cause.getAuth (and
rootAuthLoader) now re-derive the current request's auth fromargs.requestinstead 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 freshRequest). The middleware stashes only identity-free options (keys, urls) on the context for the re-derivation to reuse, the same way@clerk/nextjsresolves auth. There is nonode:async_hooksdependency, 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-runsauthenticateRequestper 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
getAuth()androotAuthLoader()now resolve auth from the current request, preventing incorrect auth during overlapping middleware execution and action→loader revalidation.