Authentication & Secure Token Storage
Almost every real app has login — and the access token it gives back must be stored in the secure Keychain, never in plain AsyncStorage.
What you will learn
- Walk through a token-based login flow
- Store a token safely with expo-secure-store (not AsyncStorage)
- Send the token on later requests and log out by deleting it
What "authentication" really means
Authentication is just the app proving who the user is — the login step. The modern way works with a token: a long secret string the server gives your app after a correct email and password. Often it is a JWT (JSON Web Token — a compact, signed string that encodes who you are and when it expires). From then on, the app sends that token with each request as a badge that says "it is still me", instead of asking for the password again.
Picture a theme park: you show your ticket once at the gate (login), get a wristband (the token), and after that you just flash the wristband on each ride (each API request) — no need to buy a ticket every time.
Why NOT AsyncStorage for the token
You met AsyncStorage earlier for small data. It is perfect for a theme or a username — but it stores data unencrypted in plain text. A token is a key to the user’s account, so it must go somewhere encrypted that the operating system protects: the iOS Keychain and the Android Keystore. Expo wraps both in one module, expo-secure-store.
| AsyncStorage | expo-secure-store | |
|---|---|---|
| Encrypted? | No — plain text | Yes — OS Keychain / Keystore |
| Good for | Theme, username, small lists | Tokens, passwords, secrets |
| Size | Larger data | Small values only |
| Use for auth tokens? | Never | Yes — this is the right place |
Install it (one-time):
npx expo install expo-secure-storeNote: Output: (Adds the secure-store module. Nothing on screen yet.)
Step 1 — Log in and get a token
Logging in is a normal fetch that POSTs the email and password (POST means "send data to the server", unlike GET which only reads). The server checks them and replies with a token. Here we call a login endpoint and read the token out of the JSON:
async function login(email, password) {
const res = await fetch('https://api.example.com/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Wrong email or password');
const data = await res.json();
return data.token; // the secret wristband
}Note: Output (on success):
The function returns a long string like eyJhbGciOiUzI1NiI..." (a JWT).
method: 'POST' sends the data; body carries the email and password as JSON; res.ok is false for a 401, so a wrong password throws instead of returning a bad token.
Step 2 — Save the token securely
Now store that token in the encrypted store. The API mirrors AsyncStorage — setItemAsync, getItemAsync, deleteItemAsync — but the value is protected by the OS:
import * as SecureStore from 'expo-secure-store';
// save after a successful login
await SecureStore.setItemAsync('userToken', token);
// read it back later (e.g. when the app reopens)
const token = await SecureStore.getItemAsync('userToken');
// delete it on logout
await SecureStore.deleteItemAsync('userToken');Note: Output:
No visible output — the token is written into the iOS Keychain / Android Keystore. Unlike AsyncStorage, another app or a file browser cannot read it. getItemAsync returns null if nothing was saved (so guard for that).
Step 3 — Send the token on future requests
For pages that need a logged-in user (the profile, the cart), you attach the token to the request in an Authorization header — a standard place to put a credential. The format Bearer THE_TOKEN is the agreed convention:
async function getProfile() {
const token = await SecureStore.getItemAsync('userToken');
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: 'Bearer ' + token },
});
return res.json();
}Note: Output: The server sees the token, knows who you are, and returns that user’s profile JSON. Without the header (or with a wrong token) the server replies 401 Unauthorized.
The whole login-to-logout flow, step by step
Tying it together, here is the full life of a session, in order:
- The app opens and calls
SecureStore.getItemAsync('userToken'). If it returns a token, the user is still logged in from last time; ifnull, show the login screen. - On the login screen the user types email and password and taps Log in, which calls our
loginfunction. loginPOSTs the credentials. The server checks them and, if correct, returns a token; a wrong password throws an error you show to the user.- You save the token with
SecureStore.setItemAsync('userToken', token)into the encrypted Keychain/Keystore, and (often) set a globaluserin your auth store so screens update. - For every protected request after that, you read the token and send it as
Authorization: Bearer <token>so the server knows it is still the same user. - When the user taps Log out, you call
SecureStore.deleteItemAsync('userToken')and clear the globaluser— the badge is gone, so the app returns to the login screen.
Watch out: Never store a password or auth token in AsyncStorage, in plain state that gets logged, or hard-coded in your source. Tokens go in secure-store only. Also give tokens an expiry on the server so a stolen one stops working.
Tip: For top-tier apps you can add biometrics (Face ID / fingerprint) to unlock the stored token, and a refresh token to get a new access token automatically when the old one expires — both build directly on this same secure-store foundation.
Q. Where should an authentication token be stored on the device?
expo-secure-store uses the iOS Keychain and Android Keystore; AsyncStorage stores plain text and must never hold tokens.✍️ Practice
- Write a
saveTokenand aloadTokenhelper usingSecureStore.setItemAsync/getItemAsync. - Write a
logoutfunction that deletes the token and explain in one line why AsyncStorage would be wrong here.
🏠 Homework
- Sketch (in code or pseudo-code) a login screen that calls a login API, saves the returned token in secure-store, and shows a profile screen only when a token exists.