Quality & Security· 5 min read

Storing Tokens Safely in React Native: Keychain, Keystore, SecureStore

Store auth tokens safely in React Native: Keychain and Keystore via expo-secure-store or react-native-keychain, token strategy, biometrics and pitfalls.

By

Full-Stack, Mobile & AI Engineer

Most React Native security problems aren't sophisticated attacks. They're tokens saved in plain AsyncStorage, API keys bundled into the JavaScript, and sessions that never expire. Each is easy to avoid once you know where secrets belong on a phone.

This guide covers React Native secure storage in practice: what the iOS Keychain and Android Keystore protect, how to use them through expo-secure-store or react-native-keychain, a sensible token strategy, biometric protection, and the pitfalls that catch even experienced teams.

Key takeaways

  • Store tokens and secrets in the iOS Keychain and Android Keystore, via expo-secure-store or react-native-keychain, never in plain AsyncStorage or unencrypted MMKV.
  • Never put real secrets in the app bundle. Anything in your JavaScript (including EXPO_PUBLIC_ variables) can be extracted.
  • Use short-lived access tokens and a refresh token, rotated on use and revocable on the server.
  • Protect sensitive actions with biometrics, not just sign-in.
  • On iOS, Keychain items can survive an app reinstall, so clear stale credentials on first launch after installation.

What the platform secure stores do

  • iOS Keychain stores small secrets encrypted by the system, tied to your app (and optionally to the device passcode or to biometrics).
  • Android Keystore keeps cryptographic keys in hardware-backed storage where available; libraries use those keys to encrypt values stored on the device.

Both are designed for small secrets: tokens, passwords, encryption keys. They're not general-purpose databases, and treating them like one leads to slow reads and size errors.

Option 1: expo-secure-store

expo-secure-store wraps the Keychain and Keystore with a simple API and works in Expo and bare apps:

import * as SecureStore from "expo-secure-store";

await SecureStore.setItemAsync("refreshToken", token, {
  keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK,
});

const refreshToken = await SecureStore.getItemAsync("refreshToken");

await SecureStore.deleteItemAsync("refreshToken");

Keep values small (store a token, not a whole user object). The keychainAccessible option controls when the item can be read; the AFTER_FIRST_UNLOCK setting lets background tasks refresh tokens, while still protecting the data until the user has entered their passcode once after the phone restarts. For values that should require biometrics on every read, the requireAuthentication option prompts Face ID or fingerprint.

Option 2: react-native-keychain

The React Native Keychain library (react-native-keychain) offers more control over access policies (for example requiring biometrics or the device passcode, and choosing security levels on Android). It suits apps with stricter requirements, such as banking or regulated data. Both libraries ultimately rely on the same platform stores.

A sensible token strategy

TokenLifetimeWhere it lives
Access tokenMinutes (for example 15)In memory; optionally Secure Store for fast resume
Refresh tokenDays to weeksSecure Store / Keychain
Session on the serverRevocableServer database
  • Rotate refresh tokens on every use, and invalidate the old one, so a stolen refresh token stops working quickly.
  • Revoke on sign-out and password change, on the server, not only on the device.
  • Handle refresh in one place (an API client interceptor), so concurrent requests don't trigger several refreshes at once.
  • Bind sensitive sessions to the device where your auth provider supports it.

Secrets that must never ship in the app

Anything included in the app bundle can be extracted by someone with the app file and some patience. That includes JavaScript constants, EXPO_PUBLIC_ environment variables and config files. So:

  • Never ship server secrets: payment secret keys, database credentials, admin API keys, third-party secrets.
  • Proxy third-party APIs through your backend when they require a secret key.
  • Treat public keys as public: Stripe publishable keys, Firebase config and similar are designed to be visible, and must be protected by server-side rules instead.

Encrypting larger local data

When you need more than a token protected (cached personal records, offline documents):

  • Encrypt MMKV with an encryption key, or use an encrypted SQLite build.
  • Generate that key once and store it in Secure Store, never hard-code it.
  • Exclude sensitive local files from device backups where your platform setup allows.
From the field: Expetitle, the real estate closing platform I built, handled personal information and documents for buyers, sellers, realtors and brokers to escrow-grade standards: role-based access, audit trails, encrypted storage and careful PII handling. On apps like that, where tokens and keys live on the device is a design decision made on day one, not a fix after a security review.

Biometrics for sensitive actions

Biometric prompts (Face ID, Touch ID, fingerprint) are most valuable at moments of risk, not only at sign-in: viewing documents, confirming payments, changing account details. With expo-local-authentication you can ask the user to authenticate before revealing or using a secret, and with Secure Store's requireAuthentication the secret itself is protected by biometrics.

Always offer a fallback (device passcode or re-entering the password), and don't treat biometrics as a replacement for server-side authorisation.

Pitfalls to avoid

  • Keychain survives reinstall on iOS. A user who deletes and reinstalls the app may find old credentials still present. Store a "has launched" flag in regular storage, and clear secure items on the first launch after installation.
  • Logging tokens. Remove tokens from logs, crash reports and analytics events.
  • Storing whole user objects in Secure Store. Keep it to small secrets; store the rest normally (encrypted if sensitive).
  • Trusting the client. Every permission check must also happen on the server.
  • No session expiry. Long-lived tokens with no rotation turn one leak into permanent access.

FAQ

Where should I store JWTs in React Native?

Store refresh tokens (and access tokens if you persist them) in secure storage backed by the iOS Keychain and Android Keystore, using expo-secure-store or react-native-keychain. Keep access tokens short-lived, ideally in memory, and never store tokens in plain AsyncStorage.

Is AsyncStorage secure for tokens?

No. AsyncStorage stores data unencrypted, so tokens there can be read from backups or compromised devices. Use the platform secure stores for secrets, and reserve AsyncStorage or MMKV for non-sensitive data.

Can I hide API keys in a React Native app?

No. Anything shipped inside the app can be extracted. Keep secret keys on your server and call third-party APIs through your backend. Keys designed to be public, like a Stripe publishable key, are fine in the app as long as server-side rules protect your data.

Why do credentials remain after reinstalling my iOS app?

iOS Keychain items can persist after an app is deleted and reinstalled. Store a simple "first launch" flag in regular storage (which is removed on uninstall), and when it's missing, clear any secure items left over from the previous install.

Next steps

If you're unsure whether your app stores tokens and secrets safely, I can review the auth flow, storage and API calls and fix the gaps before they become an incident. Get in touch, or see my services.

Sources

Bhuwanesh Sisodia

Bhuwanesh Sisodia

Full-Stack, Mobile & AI Engineer

Bhuwanesh Sisodia is a Full-Stack, Mobile & AI Engineer based in Mumbai, with 7+ years of experience and 50+ apps shipped to the App Store and Google Play. He works independently with startups and product teams worldwide.

About meGitHubLinkedInX

Related