Sign users in on mobile — run an OAuth login with expo-auth-session, keep the token in the secure store, and gate the app behind Face ID or a fingerprint.
Why: mobile auth almost always uses OAuth/OIDC — your app opens the provider’s login page in a secure system browser, the user signs in, and you receive a token. You store that token securely and send it with every API request. Three pieces do the work: expo-auth-session runs the login flow, expo-secure-store keeps the token, and your API trusts the token on each request.
app ─opens─▶ provider login (secure browser) ─user signs in─▶ token
│ │
▼ ▼
expo-auth-session (runs the flow) expo-secure-store (keeps it)
│
every API request ──Authorization: Bearer <token>─┘Why: expo-auth-session opens the provider’s login in a secure system browser (NOT a webview — providers reject those) and returns the result to your app via a redirect. useAuthRequest builds the request; promptAsync starts it; the response carries the code/token. Note: makeRedirectUri generates the right redirect for Expo Go and standalone builds.
// npx expo install expo-auth-session expo-web-browser
import * as WebBrowser from 'expo-web-browser'
import { useAuthRequest, makeRedirectUri } from 'expo-auth-session'
WebBrowser.maybeCompleteAuthSession()
const discovery = {
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenEndpoint: 'https://oauth2.googleapis.com/token',
}
function SignIn() {
const [request, response, promptAsync] = useAuthRequest(
{
clientId: 'YOUR_CLIENT_ID',
scopes: ['openid', 'profile', 'email'],
redirectUri: makeRedirectUri(),
},
discovery
)
return (
<Button title="Sign in" disabled={!request} onPress={() => promptAsync()} />
)
}Why: the token is a credential — never put it in AsyncStorage. Keep it in expo-secure-store (the device keychain/keystore), exactly as in the Local Storage lesson, then attach it to every API request. On sign-out, delete it.
import * as SecureStore from 'expo-secure-store'
// after the login succeeds:
await SecureStore.setItemAsync('auth_token', token)
// attach it to requests:
const token = await SecureStore.getItemAsync('auth_token')
fetch('https://api.example.com/me', {
headers: { Authorization: 'Bearer ' + token },
})
// on sign-out:
await SecureStore.deleteItemAsync('auth_token')Why: a returning user should not re-run the whole OAuth flow — you keep the token and unlock the app with biometrics. expo-local-authentication checks the device supports it and prompts Face ID, Touch ID, or a fingerprint. Note: this gates access to a token you already hold; it is a convenience layer, not a replacement for server-side auth.
// npx expo install expo-local-authentication
import * as LocalAuthentication from 'expo-local-authentication'
async function unlock() {
const hasHardware = await LocalAuthentication.hasHardwareAsync()
const enrolled = await LocalAuthentication.isEnrolledAsync()
if (!hasHardware || !enrolled) return false
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Unlock with Face ID',
})
return result.success // true → reveal the app, false → stay locked
}Note: rather than wiring OAuth yourself, a managed auth provider gives you a drop-in SDK with sign-in UI, social logins, and session handling. Firebase Auth, Supabase Auth, and Clerk are the common React Native choices — they run the flow and store the token for you. You still gate sensitive screens and verify the token on your backend. The underlying concepts (JWT, sessions, OAuth) are covered in the Authentication course.
roll your own ─ expo-auth-session + expo-secure-store (full control)
managed ─ Firebase / Supabase / Clerk (drop-in SDK + hosted UI)
either way ─ verify the token on YOUR backend; gate sensitive screens