React, Off the Web
Auth and persistence
Lesson 4 of 5
What you'll learn
- Choose between expo-secure-store and AsyncStorage for each kind of data
- Implement the standard session pattern: restore token on launch, verify, refresh
- Understand why native OAuth goes through a hosted browser flow, not hand-rolled tokens
Web auth leans on the browser: httpOnly cookies ride along on every request, and the server owns the session. A native app has no cookie jar you can trust and no origin — you hold the token yourself and decide where it lives. Expo gives you two very different boxes:
| | expo-secure-store | @react-native-async-storage/async-storage |
| --- | --- | --- |
| Backed by | iOS Keychain / Android Keystore | Unencrypted on-device storage |
| Encrypted | Yes | No |
| Size | Small values (~2 KB) | Larger blobs, fine for JSON |
| Use for | Tokens, keys, anything secret | Cached data, preferences, drafts |
The session pattern almost every app converges on:
import * as SecureStore from "expo-secure-store";
const KEY = "session_token";
export async function saveSession(token: string) {
await SecureStore.setItemAsync(KEY, token);
}
// On launch: restore, verify with the server, refresh or clear.
export async function restoreSession(): Promise<string | null> {
const token = await SecureStore.getItemAsync(KEY);
if (!token) return null;
const res = await fetch("https://api.example.com/me", {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
await SecureStore.deleteItemAsync(KEY);
return null;
}
return token;
}
Your root layout awaits restoreSession() behind the splash screen, then redirects into (auth) or (tabs) — the route groups from lesson 2 earning their keep.
OAuth: use the browser, on purpose
For "Sign in with Google/Apple/your IdP", resist the urge to collect credentials in your own UI or to hand-roll a native token exchange. The robust pattern is a hosted, browser-based SSO flow: open your provider's web login in a secure in-app browser session (expo-web-browser / expo-auth-session), let the user authenticate there, and receive the callback on a redirect URI (deep link) that lands back in your app. You get the provider's real login page, existing web sessions, 2FA, and passkeys for free — and one identity pipeline shared with your web app. Hand-rolled native token exchanges routinely desync from the hosted flow (different user identifiers, different claims), and merging those accounts later is painful.
Offline-first state
Mobile networks vanish in elevators. The baseline pattern: keep server data in memory (your query library of choice), mirror the last-known snapshot into AsyncStorage, and rehydrate it on launch so the app opens showing real content — stale, labeled, but present — while a background refetch reconciles. Writes made offline go into a queue that drains when connectivity returns.
Secrets in the right box
The refresh token belongs in SecureStore; the cached profile JSON belongs in AsyncStorage. Never the reverse: AsyncStorage is readable on a compromised device, and SecureStore is too small for data blobs.
The challenge models launch-time session restore: a "device" whose storage outlives each app process.
Sign in, then hit 'Restart app' — state resets but SecureStore survives, so restoreSession() finds the token. Sign out and restart to see the logged-out path.
Where should a session refresh token live in an Expo app?
Next: getting all of this onto real phones — EAS Build, store submission, and over-the-air updates.
Saved on this device. Sign in to sync your progress everywhere.