SecureStore + Keychain

Where tokens go on iOS + Android.

6 minmedium

Where you store the user's JWT matters. AsyncStorage is plaintext on disk — anyone with file-system access reads it. SecureStore (iOS Keychain, Android Keystore) is OS-encrypted. Use it. Here's how.

The right primitive — expo-secure-store

Terminal
$pnpm add expo-secure-store

Already in the Grit mobile scaffold. Stores values in:

  • iOS: Keychain — encrypted by the device's secure enclave, tied to the app's bundle ID
  • Android: Keystore + EncryptedSharedPreferences — encrypted by hardware-backed AES, tied to app signature

Both survive app updates. Both wipe on uninstall. Both refuse access from other apps.

The auth helper

apps/mobile/lib/auth-storage.ts
import * as SecureStore from 'expo-secure-store'
const ACCESS_KEY = 'grit.access_token'
const REFRESH_KEY = 'grit.refresh_token'
export async function saveTokens(access: string, refresh: string) {
await SecureStore.setItemAsync(ACCESS_KEY, access)
await SecureStore.setItemAsync(REFRESH_KEY, refresh)
}
export async function loadTokens() {
return {
access: await SecureStore.getItemAsync(ACCESS_KEY),
refresh: await SecureStore.getItemAsync(REFRESH_KEY),
}
}
export async function clearTokens() {
await SecureStore.deleteItemAsync(ACCESS_KEY)
await SecureStore.deleteItemAsync(REFRESH_KEY)
}

Wiring it into useAuth

apps/mobile/hooks/use-auth.ts (excerpt)
export function AuthProvider({ children }) {
const [tokens, setTokens] = useState<{ access?: string; refresh?: string }>({})
const [isLoading, setLoading] = useState(true)
// Restore on app cold start
useEffect(() => {
loadTokens().then((stored) => {
setTokens(stored)
setLoading(false)
})
}, [])
async function login(email: string, password: string) {
const res = await api.post('/api/auth/login', { email, password })
await saveTokens(res.data.access_token, res.data.refresh_token)
setTokens({ access: res.data.access_token, refresh: res.data.refresh_token })
}
async function logout() {
await clearTokens()
setTokens({})
}
return (
<AuthContext.Provider value={{ tokens, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)
}

On app start, we read tokens from SecureStore. If they exist, the user is already signed in. No login screen unless they explicitly logged out.

SecureStore is async. Don't render the app until isLoading is false — otherwise the route guard may send the user to /login while the token is still being loaded. Show a splash screen until ready.

What NOT to do

// AsyncStorage — plaintext on disk
await AsyncStorage.setItem('token', accessToken) // bad for JWTs
// localStorage — there isn't one in React Native
// Don't try to use the web localStorage shim

AsyncStorage is fine for non-secret preferences (theme, last-used filter). For tokens, passwords, API keys — SecureStore only.

Encryption-at-rest only matters if the device is unlocked-then-compromised

Once a phone is unlocked, processes that run as you can read your Keychain. The defence model is "another app, or someone with file- system access (e.g., a backup extraction), can't read your tokens." SecureStore covers that. It doesn't protect against malware that runs in your app's sandbox.

Quick check

A teammate uses AsyncStorage to save the JWT. Backups of the phone leak the storage file. What's exposed?

Try it

Confirm SecureStore is doing its job:

  1. Sign in on your mobile app
  2. Add a debug button that calls SecureStore.getItemAsync('grit.refresh_token') and shows the result in an alert
  3. Confirm the refresh token is there
  4. Force-kill the app, reopen it. You should still be signed in (the AuthProvider loaded the tokens on cold start).
  5. Paste a screenshot of the debug alert in notes.md.

What's next

Last lesson of the chapter — silent refresh on 401. Access tokens expire; refresh tokens save the user from re-login.

Spot a typo? Have an idea?

Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled — suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.

Suggest an improvement on GitHub