Store Auth Tokens Safely in Expo with SecureStore – A Practical Guide
Learn how Expo’s SecureStore lets you keep auth tokens encrypted, persistent, and app‑only, with a clear example, size limits, and practical checks.
20 Jul 2025, 14:02 UTC

Problem: Where to Keep Auth Tokens in a Managed Expo App
When you build a mobile app that talks to a backend, you usually receive a short‑lived access token and a longer‑lived refresh token. Storing these tokens in plain text (e.g., AsyncStorage) exposes them to anyone who can access the device’s file system. On iOS this means the token could be read from a jailbroken device; on Android it could be extracted from a rooted phone or from a backup file. The question is: how do you keep these secrets out of reach while still using Expo’s managed workflow?
Thesis: Expo SecureStore is the Pragmatic Choice for Sensitive Key/Value Pairs
Expo provides expo-secure-store, a thin wrapper around the native iOS Keychain and Android Keystore. It implements:
- Encryption at rest – the data is stored in a protected OS‑managed container.
- App‑level isolation – only the app’s bundle ID can read the stored values.
- Automatic cleanup – data is removed on uninstall.
- No need to eject – the module is available in the standard managed workflow.
Because it uses the platform’s own secure storage, you don’t have to manage cryptographic keys yourself or ship a native module. For most token‑storage scenarios, this balances security, simplicity, and maintainability.
Worked Example: Storing and Retrieving a Refresh Token
The API is intentionally simple: two async functions, setItemAsync and getItemAsync. Below is a minimal example that saves a token on login and reads it on app start. Replace 'refresh_token' with your own key if you prefer.
import { useEffect, useState } from 'react';
import * as SecureStore from 'expo-secure-store';
const TOKEN_KEY = 'refresh_token';
export function useAuthToken() {
const [token, setToken] = useState(null);
// Load token once when the component mounts
useEffect(() => {
const loadToken = async () => {
try {
const stored = await SecureStore.getItemAsync(TOKEN_KEY);
if (stored) {
setToken(stored);
}
} catch (e) {
console.warn('SecureStore getItemAsync failed', e);
}
};
loadToken();
}, []);
// Helper to persist a new token
const persistToken = async (newToken) => {
try {
await SecureStore.setItemAsync(TOKEN_KEY, newToken, {
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});
setToken(newToken);
} catch (e) {
console.warn('SecureStore setItemAsync failed', e);
}
};
return { token, persistToken };
}
Key points to note:
keychainAccessibleis optional; the default isWHEN_UNLOCKEDon iOS andENCRYPTEDon Android. SettingTHIS_DEVICE_ONLYensures the value cannot be backed up to iCloud or Google Drive.- All calls are awaited; the functions return promises.
- The token is automatically cleared when the app is uninstalled – no manual cleanup needed.
Verification Checklist
- Persistency Test: Store a short string (e.g.,
'test123'), close the app, reopen, and confirmgetItemAsyncreturns the same value. - Encryption Check: On a physical device, use Xcode’s Keychain Access (iOS) or Android Studio’s Keystore Explorer to locate the entry. Verify it is stored as an encrypted blob.
- Size Limit Test: Attempt to store a string longer than the documented limit (≈4 KB on iOS, ≈8 KB on Android). You should receive an error or see the value truncated; the API does not silently accept oversized data.
Trade‑offs & Limitations
While SecureStore is great for short secrets, it is not a silver bullet for all storage needs:
- Size Constraints: The per‑item limit means you cannot store large JWTs or blobs. If your token exceeds the limit, you must split it or use a different storage strategy.
- Hardware Security Module (HSM) Variability: On some Android devices, the Keystore may fall back to software encryption. This is device‑dependent; you cannot guarantee HSM use across all models.
- No Built‑in Sync: SecureStore does not sync across devices. If your user logs in on multiple devices, you need to handle token exchange separately.
- Limited API: The module only offers basic key/value operations. For more advanced features (e.g., conditional access, rotation), you’ll need to implement additional logic.
Actionable Takeaways
- Use
expo-secure-storefor any data that must be kept confidential: access tokens, refresh tokens, API keys, or small secrets. - Keep the stored value under the documented size limit. If you need to store more, consider encrypting the data yourself and splitting it across multiple keys.
- Set
keychainAccessibletoWHEN_UNLOCKED_THIS_DEVICE_ONLYif you want to prevent backups. - Test persistence and size limits on the target devices you support.
- Document the decision in your architecture so future developers understand why SecureStore was chosen and what its constraints are.
By following this approach, you can keep your authentication secrets safe without leaving Expo’s managed workflow or adding a native module.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.