-
Treat the mobile client as untrusted: Assume attackers can extract bundled values, hook native APIs, and replay tokens from compromised devices - design sessions that limit blast radius with short access TTL and refresh rotation.
-
Never store refresh tokens in AsyncStorage: Unencrypted SQLite is trivially readable on rooted devices - use expo-secure-store with WHEN_UNLOCKED_THIS_DEVICE_ONLY (OWASP M2: Insecure Data Storage).
-
Keep access tokens short-lived: 5–15 minute access token TTL limits damage from interception - refresh proactively before expiry, not only after 401 (OWASP M4: Insecure Authentication).
-
Rotate refresh tokens on every exchange: Issue a new refresh token and invalidate the previous one - detect reuse as a breach signal and revoke the entire token family (OWASP M4).
-
Implement logout everywhere: Local sign-out is insufficient for stolen devices - expose server-side refresh revocation that invalidates all sessions for the user (OWASP M4).
-
Clear SecureStore, Query cache, and navigation on logout: deleteItemAsync for all auth keys, queryClient.clear(), and router.replace to sign-in - Android back must not reveal authenticated screens (OWASP M2).
-
Use Authorization Code + PKCE for OAuth: Mobile is a public client - never implicit flow (response_type=token) or embedded WebViews; use expo-auth-session with usePKCE: true (OWASP M4).
-
Exchange OAuth codes on your backend when secrets exist: Client secrets do not belong in the app bundle - send code + code_verifier to your server for token exchange (OWASP M1: Improper Platform Usage).
-
Register redirect URIs per build flavor: makeRedirectUri() output must match Google, Apple, and IdP consoles for dev, preview, and production - redirect_uri_mismatch is the top OAuth integration failure.
-
Never put OAuth client secrets in EXPO_PUBLIC_*: Public env vars inline into the Hermes bundle - audit with npx expo config --type public before every release (OWASP M2).
-
Attach tokens via Authorization: Bearer - not URL query params: Query strings land in logs, analytics, and referrer headers - headers only for access tokens (OWASP M3: Insecure Communication).
-
Use HTTPS for all auth and API endpoints: No cleartext exceptions in production - system TLS on SDK 57 is the default; certificate pinning requires an ADR (OWASP M3).
-
Single 401 retry with deduplicated refresh: One in-flight refresh promise for parallel 401s - infinite retry loops mask misconfiguration and hammer the auth server (OWASP M4).
-
Distinguish offline from revoked sessions: Network failures during refresh should not force sign-out - session_expired_offline vs session_expired prevents airplane-mode logout rage (OWASP M7: Client Code Quality).
-
Proactive refresh on foreground resume: AppState listener refreshes when shouldRefresh(session) after background - users should not see hourly re-login prompts (OWASP M4).
-
Apply clock skew buffer to client expiry: Refresh 30–120 seconds before client expiresAt - prefer server serverTime in refresh responses over device clock alone (OWASP M5: Insufficient Cryptography).
-
Biometrics prove presence - not identity to the server: Use expo-local-authentication for step-up UX; high-risk endpoints still need server MFA or step-up tokens (OWASP M4).
-
Offer passcode fallback for biometric flows: disableDeviceFallback: false - trapping users without enrolled biometrics fails accessibility and App Review expectations (OWASP M1).
-
Namespace SecureStore keys per account: Multi-account apps use auth.refresh_token.{accountId} - switch account purges TanStack Query and Zustand before bootstrap (OWASP M2 / M6).
-
Bind refresh tokens to device ID server-side: Send stable deviceId on login and refresh - server rejects unknown devices; users can revoke lost devices from settings (OWASP M4).
-
Include userId in TanStack Query cache keys: Prevents cross-account data flash during switch even if purge races - queryKey: ['orders', userId] (OWASP M6: Insecure Authorization).
-
Never trust client-side JWT claims for authorization: Decode claims for display only - server validates roles and scopes on every request (OWASP M6).
-
Scrub tokens from logs and crash reports: Redact Authorization headers in Sentry breadcrumbs and structured logging - tokens in crash dumps are a common audit finding (OWASP M2).
-
Degrade gracefully on jailbreak/root signals: High-risk actions (payments, PII export) warrant blocks or MFA - do not hard-lock entire apps on spoofable signals without product/legal approval (OWASP M8: Code Tampering).
-
Test auth on development builds - not only Expo Go: OAuth redirect URIs, Keychain access groups, and Apple Sign In entitlements differ from Expo Go - E2E auth tests run against real bundle IDs before store submission (OWASP M9: Reverse Engineering).
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (expo ~57.0.4).