fix(expo): Prevent stale native clients from replacing signed-in sessions#9222
Conversation
🦋 Changeset detectedLatest commit: da2894c 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.
|
@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: |
There was a problem hiding this comment.
Really nice work tracking this down — I went through the FAPI side to sanity-check the assumptions and the foreign-sessionless rejection holds up well against what the server actually does (a stale token on GET /v1/client re-mints a lazy client reusing the same id, so the validation fetch is deterministic). I found one interaction with the 401 recovery path that I think can still reproduce the original bug, plus two smaller things — details inline.
📝 WalkthroughWalkthroughExpo native client synchronization now distinguishes token-cache timeouts from confirmed missing tokens, validates refreshed clients against the current JS client, restores prior device tokens when native verification fails, and rejects foreign sessionless clients. Recovery and bootstrap paths use the updated logic. The Expo fixture adds Android token-divergence controls and an end-to-end regression flow, with provider tests covering the synchronization scenarios. Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
Warning Review ran into problems🔥 ProblemsLinked repositories: Couldn't analyze
Errors logged to '/home/jailuser/git/.git/lfs/logs/20260724T181338.342058045.log'. Errors logged to '/home/jailuser/git/.git/lfs/logs/20260724T181458.65044682.log'. Comment |
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.
Actionable comments posted: 1
🤖 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/expo/src/provider/nativeClientSync.tsx`:
- Around line 932-934: Update the bootstrap cache reads around
initialJsDeviceToken and the corresponding second read to preserve the timeout
sentinel returned by getCachedDeviceToken instead of coercing it to null. Ensure
syncClientStateFromJs is invoked only when the cache result is authoritative,
while retaining null for a confirmed missing token.
🪄 Autofix (Beta)
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: 658231b0-2756-4fda-b111-cbbeaa9c707b
📒 Files selected for processing (3)
.changeset/protect-expo-native-client-sync.mdpackages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsxpackages/expo/src/provider/nativeClientSync.tsx
🔗 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)
| let initialJsDeviceToken: string | null = null; | ||
| try { | ||
| initialJsDeviceToken = await getCachedDeviceToken(tokenCache); | ||
| initialJsDeviceToken = (await getCachedDeviceToken(tokenCache)) ?? null; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant file around the bootstrap logic and the native sync call.
sed -n '900,980p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n---\n'
sed -n '1,260p' packages/expo/src/provider/nativeClientSync.tsx
# Find the implementation and call sites of ClerkExpo.syncClientStateFromJs.
printf '\n=== syncClientStateFromJs references ===\n'
rg -n "syncClientStateFromJs" packages/expo packages -g '!**/dist/**' -g '!**/build/**' || true
# Inspect the native bridge / implementation if present.
printf '\n=== ClerkExpo implementation candidates ===\n'
fd -H "ClerkExpo" packages/expo packages -t f || trueRepository: clerk/javascript
Length of output: 29941
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '=== iOS module ===\n'
sed -n '80,180p' packages/expo/ios/ClerkExpoModule.swift
printf '\n=== iOS native bridge ===\n'
sed -n '291,380p' packages/expo/ios/ClerkNativeBridge.swift
printf '\n=== Android module ===\n'
sed -n '387,470p' packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
printf '\n=== Native module type signature ===\n'
sed -n '1,80p' packages/expo/src/specs/NativeClerkModule.ts
sed -n '1,80p' packages/expo/src/specs/NativeClerkModule.android.ts
printf '\n=== Bootstrap-related tests around null device token ===\n'
sed -n '280,330p' packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsx
sed -n '1110,1210p' packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsxRepository: clerk/javascript
Length of output: 13840
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '=== getCachedDeviceToken helper ===\n'
rg -n "function getCachedDeviceToken|getCachedDeviceToken\\(" packages/expo/src/provider/nativeClientSync.tsx
sed -n '560,700p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n=== bootstrap branch around currentJsDeviceToken ===\n'
sed -n '940,980p' packages/expo/src/provider/nativeClientSync.tsx
printf '\n=== tests covering timeout / null cache behavior ===\n'
rg -n "timeout|undefined|null" packages/expo/src/provider/__tests__/ClerkProvider.nativeClientSync.test.tsxRepository: clerk/javascript
Length of output: 11901
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '420,470p' packages/expo/src/provider/nativeClientSync.tsxRepository: clerk/javascript
Length of output: 1739
Preserve the timeout sentinel in bootstrap
Both bootstrap reads collapse a timed-out cache lookup back to null, so a slow second read becomes indistinguishable from a confirmed missing token. That can send bootstrap down the JS→native sync path on an unreliable snapshot. syncClientStateFromJs(null, ..., true, true) doesn’t clear native state, but it still forces a refresh that should be skipped until the cache read is authoritative.
🤖 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/expo/src/provider/nativeClientSync.tsx` around lines 932 - 934,
Update the bootstrap cache reads around initialJsDeviceToken and the
corresponding second read to preserve the timeout sentinel returned by
getCachedDeviceToken instead of coercing it to null. Ensure
syncClientStateFromJs is invoked only when the cache result is authoritative,
while retaining null for a confirmed missing token.
mikepitre
left a comment
There was a problem hiding this comment.
@wobsoriano I didn't test manually but if all looks good with your manual testing then the code itself looks good to me!
There was a problem hiding this comment.
🧹 Nitpick comments (1)
integration/templates/expo-native/components/E2EControls.tsx (1)
11-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd explicit public function return types.
The new exported components omit explicit return types;
GoogleSignInButtonalso lacks public API documentation.
integration/templates/expo-native/components/E2EControls.tsx#L11-L33: add the component and async-handler return types.integration/templates/expo-native/components/GoogleSignInButton.tsx#L5-L24: add JSDoc and the component return type.integration/templates/expo-native/components/JsSignInForm.tsx#L10-L31: add the component and helper return types.🤖 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 `@integration/templates/expo-native/components/E2EControls.tsx` around lines 11 - 33, Add explicit public return types to E2EControls and its async handlers onCorruptNativeToken and onMintSessionToken in integration/templates/expo-native/components/E2EControls.tsx; document and type GoogleSignInButton in integration/templates/expo-native/components/GoogleSignInButton.tsx; and type JsSignInForm plus its helper functions in integration/templates/expo-native/components/JsSignInForm.tsx. Use return types matching each function’s existing behavior and add the requested JSDoc for GoogleSignInButton.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 `@integration/templates/expo-native/components/E2EControls.tsx`:
- Around line 11-33: Add explicit public return types to E2EControls and its
async handlers onCorruptNativeToken and onMintSessionToken in
integration/templates/expo-native/components/E2EControls.tsx; document and type
GoogleSignInButton in
integration/templates/expo-native/components/GoogleSignInButton.tsx; and type
JsSignInForm plus its helper functions in
integration/templates/expo-native/components/JsSignInForm.tsx. Use return types
matching each function’s existing behavior and add the requested JSDoc for
GoogleSignInButton.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 74277d0f-f190-4562-9ee7-29c83dcf2bef
📒 Files selected for processing (8)
integration/templates/expo-native/App.tsxintegration/templates/expo-native/components/E2EControls.tsxintegration/templates/expo-native/components/GoogleSignInButton.tsxintegration/templates/expo-native/components/JsSignInForm.tsxintegration/templates/expo-native/modules/e2e-hooks/android/build.gradleintegration/templates/expo-native/modules/e2e-hooks/android/src/main/java/com/clerk/e2ehooks/E2EHooksModule.ktintegration/templates/expo-native/modules/e2e-hooks/expo-module.config.jsonintegration/tests/expo-native/flows/session-survives-native-token-divergence.yaml
🔗 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/clerk-ios(auto-detected)clerk/cli(auto-detected)clerk/clerk-android(auto-detected)
Description
Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change