What you'll learn
- Store tokens securely
- Explain why a mobile app is a public client that can hold no secret
- Refresh an expired session once and sign out cleanly if refreshing fails
- Use OAuth with PKCE in the system browser for third-party sign-in
Store tokens securely, refresh sessions safely and use OAuth with PKCE on a public client.
Why it matters
Almost every production app has accounts, and the mobile side of authentication is where many security mistakes happen. A phone can be lost, stolen, backed up, or rooted, and your app's code is in the user's hands, so anyone can unpack it and read it. Mobile auth has to assume all of that.
What is different from the web
- No httpOnly cookies by default. Web apps can keep a session in a cookie the JavaScript cannot read. Mobile apps usually hold tokens themselves and send them in an
Authorization: Bearer <token>header, so where you store the token matters. - Your bundle is public. Anything in your JavaScript (API keys, client secrets) can be extracted from the app package. Treat the app as a public clientAn app whose code is in users' hands, so it cannot keep any secret such as an API key or client secret.: it can hold no secret.
- Long sessions. Users expect to stay signed in for weeks, so apps pair a short-lived access tokenA short-lived token sent with every API call to prove who the user is. with a longer-lived refresh tokenA longer-lived token used to get a new access token without signing in again; store it in secure storage..
- BiometricsFace ID or a fingerprint checked by the OS to unlock a credential already on the device; not a login by itself. (Face ID, fingerprint) can unlock a stored credential locally, but they are not a login by themselves.
Storing tokens securely
Use the platform's secure storage:
- iOS KeychainThe iOS encrypted store for credentials; with the Android Keystore it backs expo-secure-store. and Android Keystore-backed encrypted storage. In Expo,
expo-secure-storewraps both. In bare React Native,react-native-keychainis common. - Not AsyncStorage, plain files, or Redux persisted to disk: those are unencrypted and can end up in backups.
- Keep tokens in memory while the app runs and read them from secure storage at startup.
import * as SecureStore from "expo-secure-store";
export async function saveSession({ accessToken, refreshToken }) {
await SecureStore.setItemAsync("refreshToken", refreshToken);
memory.accessToken = accessToken; // short-lived; memory is enough
}
export async function clearSession() {
await SecureStore.deleteItemAsync("refreshToken");
memory.accessToken = null;
}SecureStore is meant for small values (a few kilobytes), which fits tokens.
Access and refresh tokens
A typical flow:
- The user signs in. The server returns an access token (valid for minutes) and a refresh token (valid for days or weeks).
- Every API call sends the access token.
- When the server answers 401, the API client calls the refresh endpoint once, stores the new tokens, and retries the original request.
- If refreshing fails, the session is over: clear storage, clear caches, and send the user to sign-in.
Two details matter in production. If five requests fail with 401 at the same moment, only one refresh should run and the others should wait for it (share a single pending promise). And servers should rotate refresh tokens (issue a new one on each refresh and revoke the old one), so a stolen token stops working quickly.
Signing in with another provider
For "Sign in with Google/Apple" or your company's identity provider, use OAuth 2.0 with PKCEProof Key for Code Exchange, an addition to OAuth 2.0 that lets apps without a client secret sign users in safely. (Proof Key for Code Exchange) in the system browser, for example with expo-auth-session or the providers' native SDKs. PKCE exists precisely because mobile apps cannot keep a client secret. Never ask for the user's Google password in your own form, and never embed a client secret in the app. Apple's guidelines require apps that offer certain third-party sign-in options to also offer an equivalent privacy-focused option such as Sign in with Apple; read the current App Review Guidelines before you ship (checked on 2026-10-23).
Biometrics
expo-local-authentication asks the OS to verify Face ID or a fingerprint and returns success or failure. Use it to gate access to a credential already on the device ("Unlock with Face ID"), with a fallback to the device passcode or a password. The server never sees the fingerprint.
Common mistakes
- Tokens in AsyncStorage.
- API secrets or admin keys in the app bundle. Put privileged calls on your server.
- Logging tokens or full request headers to the console or a crash reporter.
- Refresh storms: many parallel refresh calls that invalidate each other.
- Sign-out that clears the token but leaves cached personal data on screen.
- Building your own password reset or 2FA from scratch when the backend already has a secure flow; the app should call the backend's endpoints.
Quick recap
- Mobile apps are public clients: no secrets in the bundle.
- Store refresh tokens in the Keychain/Keystore (expo-secure-store), keep access tokens short-lived.
- On 401, refresh once, share the pending refresh, retry, and sign out cleanly if it fails.
- Use OAuth with PKCE in the system browser for third-party sign-in; biometrics unlock local credentials.
Try it
Take the quiz in the Do tab. Then look at the auth endpoints you built in Back-End Web APIs and decide which token each would return to a mobile client.
Key terms (6)
- public client
- An app whose code is in users' hands, so it cannot keep any secret such as an API key or client secret.
- refresh token
- A longer-lived token used to get a new access token without signing in again; store it in secure storage.
- access token
- A short-lived token sent with every API call to prove who the user is.
- PKCE
- Proof Key for Code Exchange, an addition to OAuth 2.0 that lets apps without a client secret sign users in safely.
- Keychain
- The iOS encrypted store for credentials; with the Android Keystore it backs expo-secure-store.
- Biometrics
- Face ID or a fingerprint checked by the OS to unlock a credential already on the device; not a login by itself.