fix(clerk-js,expo): harden native session-minter token path - #9284
fix(clerk-js,expo): harden native session-minter token path#9284nikosdouvlis wants to merge 8 commits into
Conversation
Native apps feed the previous session token to the edge Session Minter as the mint seed, so a stale or regressed lastActiveToken is no longer just a stale local token, it becomes the input to the next mint and the staleness chains forward. Three gaps made that reachable on the Expo/native path. clerk-js: Client.fromJSON rebuilds every session object on a client update, and the rebuilt objects replaced the live ones while unconditionally adopting the payload's last_active_token. A piggybacked response carrying an older token could therefore regress the active session's token. fromJSON now carries the freshest of the prior instance's token and the payload token, using the same same-sid/same-org oiat guard that already protects the in-place path, so a genuine session or org switch still adopts the incoming token while a stale piggyback cannot win. Token-clearing on a token-less payload is preserved. expo: a failed initial load substituted dummy resources for both environment and client, wiping auth_config.session_minter for the whole instance lifetime even when a good cached environment existed; it now substitutes only the missing resource. The patched 401 handler ran full native-state recovery plus a client refetch on every 401; a short cooldown collapses a burst to one cycle, and a rotated device token clears the cooldown so fresh native identity always gets a fresh attempt. tokenCache doc corrected to say it stores the client JWT.
🦋 Changeset detectedLatest commit: 5bff88b The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 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 |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
@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 |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughClerk JS now preserves the freshest Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
…e device token The native 401 cooldown clears whenever the device-token cache changes, so a fresh external identity gets a fresh recovery attempt. But a failed recovery rolls the device token back to its previous value, and that rollback write fired the same listener and cleared the cooldown, so a second 401 inside the window re-ran full recovery, reopening the storm on exactly the failing-recovery path the cooldown is meant to bound. Route the rollback write on the 401 path through the notification suppression recovery already uses for its own writes, so a rollback no longer clears the cooldown. The native-client-event recovery path keeps notifying, since there the rollback notification is load-bearing: it queues the native refresh that pushes the restored token back to the native module.
The suppressed rollback write skips the token-cache listener that used to resync native, so rejecting a foreign client left native holding the rejected token. The reject branch now pushes the restored token to native directly. The error branch stays cache-only since its second-chance recovery re-adopts the native token anyway. The cooldown stamp moves to when the attempt settles, so a slow recovery no longer finishes with a mostly spent window. A rotation landing mid-attempt still clears the stamp and forces a fresh attempt, and a backwards clock jump counts as expired instead of waiting out the gap. Persisting the dummy client snapshot made the next boot see a populated cache and skip recovery. The save listener now skips the dummy, and a previously persisted dummy is treated as missing on load.
0942f8a to
b6a7f83
Compare
The dummy-client save guard skipped the whole listener block, including the SessionJWTCache.remove() that wipes the offline JWT fallback on a sessionless emission. getToken falls back to that cache on network errors regardless of which client is active, so the wipe is load-bearing and is now restored for dummy emissions. The environment cache had the symmetric hole the client guard closed: an offline boot with only the environment missing persisted the dummy environment snapshot, and the next boot saw both caches populated and never scheduled recovery. The save listener now skips dummy environment snapshots and a previously persisted one is treated as missing on load.
a71d712 to
be1d6d5
Compare
…ardening # Conflicts: # packages/clerk-js/bundlewatch.config.json
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
shouldKeepExistingLastActiveToken moves to tokenFreshness next to the primitives it composes, and Client.fromJSON assigns the carried-forward token directly, dropping the one-caller __internal method. The redundant normalizeOrgId wrap goes; tokenOrgId already returns an empty string. refreshJsClientFromNativeState reports 'refreshed' or 'restored' instead of taking a controller ref, so the 401 caller owns the restored-token push to native and the shared helper stays policy-free. The rollback write branch collapses into one syncNativeDeviceTokenToCache call. Dummy detection gets isDummyClient/isDummyEnvironment predicates beside the constants; the environment guard now runs before the snapshot it used to discard. Tests: the 401 foreign-client push assertion folds into the existing recovery test, the piggyback guard is covered by a direct Client.fromJSON test instead of 45 lines of fake-clerk wiring, and the three hand-rolled resource-cache stubs become one.
Why
Native SDKs are getting Session Minter support: the app sends its previous session token to the edge as the seed the new token is minted from. That raises the stakes on the token clerk-js holds. A stale or regressed lastActiveToken is no longer just a stale local token, it becomes the input to the next mint, so staleness chains forward instead of self-correcting. This closes the Expo/native gaps that let that happen.
What changed
clerk-js: Client.fromJSON rebuilds every session object on a client update and used to adopt the payload's last_active_token unconditionally, so a piggybacked older token could regress the active session's token. It now carries the freshest of the prior instance's token and the incoming one through the same sid/org oiat guard the in-place path already uses, so a genuine session or org switch still adopts the incoming token while a stale piggyback cannot win. Clearing on a token-less payload is preserved.
expo: a failed initial load used to substitute dummy resources for both environment and client, wiping auth_config.session_minter for the whole instance lifetime; it now keeps a good cached environment when only the client cache is missing. The native 401 handler gets a short cooldown so a burst collapses to a single native-recovery cycle, and a device-token rotation observed through the token cache reopens the cooldown so fresh native identity gets a fresh attempt. tokenCache doc corrected to say it holds the client JWT.
Related
Part of a four-PR set adding native Session Minter support. This one covers clerk-js and Expo; the edge worker (cloudflare-workers), clerk-ios, and clerk-android land alongside it.