fix(backend): Scope the JWKS cache per Clerk instance - #9394
fix(backend): Scope the JWKS cache per Clerk instance#9394dominic-clerk wants to merge 2 commits into
Conversation
The module-level JWKS cache was keyed on the bare `kid`. Because a Clerk `kid` is the instance id, a key cached for one instance was a direct hit for another instance's verification in the same process, and the lookup short-circuits before `secretKey` is consulted. Since `verifyJwt` never asserts `iss`, a session token minted by instance B authenticated against instance A in any process serving both — the documented Dynamic Keys / multi-tenant pattern. The same cache backs the M2M and OAuth sinks via `resolveKeyAndVerifyJwt`. Remote keys are now cached per `(apiUrl, apiVersion, secretKey)`, each namespace carrying its own TTL, so a cross-instance lookup misses and forces the secret-key-authenticated fetch. Local PEM keys move to their own store, which also stops a local `jwtKey` from disabling the remote TTL process-wide. The `jwk-kid-mismatch` message no longer enumerates cached kids, which disclosed the instance ids warm in a shared process. SDK-148
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 8808214 The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
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 |
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/electron
@clerk/electron-passkeys
@clerk/eslint-plugin
@clerk/expo
@clerk/expo-google-signin
@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 |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
📝 WalkthroughWalkthroughThe backend derives local PEM keys on each call and stores remote JWKS keys in caches scoped by API URL, API version, and secret key. Each remote cache has independent expiration. Missing-key errors no longer list cached key IDs. Tests cover local key derivation, cache isolation, TTL behavior, and error output. Estimated code review effort: 3 (Moderate) | ~20 minutes Mergeability Score: ⚪ Minimal · up to The change scopes JWKS caching per Clerk instance and separates local key handling, preventing cross-instance key reuse while preserving normal verification behavior; no actionable merge-blocking risk remains beyond normal checks and review. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@packages/backend/src/tokens/__tests__/keys.test.ts`:
- Around line 229-266: Extend the cache-isolation tests in keys.test.ts to cover
apiUrl and apiVersion without using skipJwksCache. For each field, keep
secretKey and kid identical across scopes, configure distinct values, assert the
second request performs an independent fetch, and verify it returns the key from
its own JWKS response.
In `@packages/backend/src/tokens/keys.ts`:
- Around line 34-47: Update getRemoteCache and the related cache-expiration
paths around cacheHasExpired to evict expired remote-cache namespaces from
remoteCaches, not merely clear their keys. Implement a bounded LRU or idle-entry
policy so unused scopes are removed while preserving reuse of active scopes and
existing key-expiration behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 190d1fcf-c228-426b-b194-97a49d8ee5d3
📒 Files selected for processing (3)
.changeset/scope-jwks-cache-per-instance.mdpackages/backend/src/tokens/__tests__/keys.test.tspackages/backend/src/tokens/keys.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
clerk/clerk_go(manual)clerk/dashboard(manual)clerk/accounts(manual)clerk/backoffice(manual)clerk/clerk(manual)clerk/clerk-docs(manual)clerk/cloudflare-workers(manual)clerk/cli(auto-detected)clerk/clerk-ios(auto-detected)clerk/clerk-android(auto-detected)
…e JWKS cache Address review: the PEM-derived JWK was cached by kid alone (an untrusted token-header value), so a warm hit could serve one instance's key to another instance's verifier. Derivation is cheap, so drop the cache entirely. Also evict expired JWKS cache scopes on new-scope creation and add apiUrl/ apiVersion cache-isolation tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Description
The module-level JWKS cache was keyed on the bare
kid. Because a Clerkkidis the instance id, a key cached for one instance was a direct hit for another instance's verification in the same process, and the lookup short-circuits beforesecretKeyis consulted. SinceverifyJwtnever assertsiss, a session token minted by instance B authenticated against instance A in any process serving both — the documented Dynamic Keys / multi-tenant pattern. The same cache backs the M2M and OAuth sinks viaresolveKeyAndVerifyJwt.Remote keys are now cached per
(apiUrl, apiVersion, secretKey), each namespace carrying its own TTL, so a cross-instance lookup misses and forces the secret-key-authenticated fetch. Local PEM keys move to their own store, which also stops a localjwtKeyfrom disabling the remote TTL process-wide.The
jwk-kid-mismatchmessage no longer enumerates cached kids, which disclosed the instance ids warm in a shared process.Fixes SDK-148
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change