JWT Authentication and Token Storage

Store and refresh JWTs safely in a React Native app: secure storage, axios interceptors, and the mistakes that leak tokens.

This guide covers handling JWT-based authentication in a React Native app: where to store tokens, how to refresh them, and the mistakes that quietly create security holes.

Quick Answer

Store access and refresh tokens in secure, encrypted device storage (expo-secure-store or react-native-keychain), never in AsyncStorage; attach the access token to requests with an interceptor; refresh it automatically on a 401; and generate a long, random JWT_SECRET on the server.

1. Access Tokens vs Refresh Tokens

Use a short-lived access token (minutes to a couple of hours) for API requests, and a longer-lived refresh token to silently obtain new access tokens without forcing a re-login. Keep the access token's lifetime short specifically because it's the one attached to every request and therefore the one most likely to leak into logs.

2. Where to Store Tokens

AsyncStorage is unencrypted, plain key-value storage on disk; anything sensitive stored there is readable by any code (or, on a rooted/jailbroken device, any user) with file access. Use:

  • expo-secure-store in an Expo-managed project, or
  • react-native-keychain in a bare React Native project.

Both back onto the platform's actual secure storage (iOS Keychain, Android Keystore).

3. Attaching the Token to Requests

Add an axios (or fetch wrapper) request interceptor that reads the access token from secure storage and sets Authorization: Bearer <token> on every outgoing request, so individual API calls don't need to remember to do it themselves.

4. Refreshing on 401

Add a response interceptor: on a 401, attempt a refresh using the refresh token, then retry the original request once with the new access token. Guard against a refresh storm if several requests fail with 401 simultaneously (e.g. right after expiry), queue them behind a single in-flight refresh call rather than firing one refresh per failed request.

5. Server-Side Secret Strength

Generate JWT_SECRET as a long, random value, not a memorable phrase:

node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"

A short or guessable secret means anyone can forge valid tokens.

Common Issues

Tokens stored in AsyncStorage move them to secure storage; this is the single most common React Native auth mistake.

Multiple simultaneous 401s trigger multiple refresh calls can invalidate a freshly issued refresh token if your server rotates them; serialize refresh attempts behind one in-flight promise.

"Invalid token" errors that only happen occasionally often clock skew between the device and server for expiry checks; allow a small leeway (a few seconds) when validating exp.

User stays logged in after "logout" the logout handler cleared app state but not secure storage; explicitly delete the stored tokens on logout.

Verification Checklist

  • access and refresh tokens are stored via expo-secure-store or react-native-keychain, not AsyncStorage;
  • requests attach the access token automatically via an interceptor;
  • a 401 triggers exactly one refresh attempt, even under concurrent requests;
  • JWT_SECRET is long and randomly generated, not a memorable string;
  • logout clears tokens from secure storage.

Next Steps