React Native JWT Authentication Using Axios Interceptors

Build secure, production-ready authentication flows with automatic token refresh and seamless error handling

Why JWT Authentication Matters for React Native

Modern mobile applications require secure, stateless authentication mechanisms that scale across platforms and integrate seamlessly with RESTful APIs. JSON Web Tokens (JWT) have become the standard for stateless authentication in mobile apps, offering scalability and flexibility for modern API architectures.

The stateless nature of JWT tokens eliminates the need for server-side session storage, reducing infrastructure complexity and enabling horizontal scaling of backend services. Each JWT contains encoded claims about the authenticated user, signed cryptographically to prevent tampering.

React Native applications face unique authentication challenges due to their mobile context. Users may experience network fluctuations, switch between WiFi and cellular connections, or have the app backgrounded for extended periods--all scenarios that can invalidate authentication tokens. A well-implemented JWT authentication system using Axios interceptors handles these scenarios gracefully, automatically refreshing expired tokens without disrupting the user experience. Unlike traditional session-based authentication, JWT allows your React Native app to remain authenticated even through network interruptions, while the interceptor pattern ensures every API request automatically includes valid credentials without duplicating authentication logic throughout your codebase.

The Axios HTTP client library has become the de facto standard for making API requests in React Native applications. Its interceptor system provides a powerful mechanism for injecting authentication logic into the request/response lifecycle, ensuring that every API call includes valid credentials and that authentication errors are handled consistently across the application. By centralizing authentication logic in interceptors, you avoid code duplication and ensure that token management policies are applied uniformly throughout your codebase. This approach also integrates seamlessly with our API development services, creating a cohesive backend architecture that scales with your application needs.

Understanding JWT Token Architecture

A complete JWT authentication implementation in React Native involves two types of tokens with distinct purposes and security characteristics. Understanding the relationship between these tokens is essential for building a secure and user-friendly authentication system.

Access Tokens

Access tokens serve as credentials for accessing protected API resources. These tokens are typically short-lived, with expiration times ranging from 15 minutes to a few hours. The short lifespan limits the window of opportunity for token theft, as a compromised access token will expire quickly. Access tokens are included in the Authorization header of API requests, typically using the Bearer scheme: Authorization: Bearer <access_token>. The server validates the token signature and expiration date on each request, rejecting any requests with invalid or expired tokens.

Refresh Tokens

Refresh tokens solve the usability problem that would arise from short-lived access tokens. Rather than requiring users to re-enter their credentials every time an access token expires, the application can exchange a valid refresh token for a new access token. Refresh tokens have much longer lifespans--often days or weeks--and can be stored more securely since they are only used for token renewal rather than direct API access. When an access token expires, the application detects the 401 Unauthorized response and initiates a refresh flow using the stored refresh token. If the refresh is successful, the new access token replaces the expired one, and any queued API requests continue. If the refresh fails (because the refresh token is also expired or revoked), the user must re-authenticate.

The Token Lifecycle

The token lifecycle begins when a user successfully authenticates with their credentials (username/password, OAuth provider, or other authentication method). The server responds with both an access token and a refresh token. The React Native application stores both tokens securely and uses the access token for subsequent API requests. When the access token expires, the application detects the 401 Unauthorized response and initiates a refresh flow using the stored refresh token. If the refresh is successful, the new access token replaces the expired one, and any queued API requests continue. If the refresh fails (because the refresh token is also expired or revoked), the user must re-authenticate.

The separation of access and refresh tokens also enables security features like token rotation and revocation. Each refresh can issue a new refresh token, invalidating the previous one and limiting the impact of token theft. When a user logs out or an administrator revokes access, the refresh token can be invalidated server-side, ensuring that no new access tokens can be issued even if the attacker possesses a stolen refresh token.

This architecture forms the foundation for secure authentication that works seamlessly with our mobile app development services, providing a consistent authentication experience across iOS and Android platforms.

JWT token lifecycle flow diagram showing authentication, API requests, token refresh, and logout

The complete JWT authentication lifecycle in a React Native application

Key Components of JWT Authentication

Building blocks for a robust authentication system

Secure Token Storage

Use React Native AsyncStorage with Keychain/Keystore integration for production-grade credential security

Axios Request Interceptors

Automatically attach valid access tokens to every API request before sending

Response Interceptors

Intercept 401 errors and trigger automatic token refresh without user interruption

Request Queue Management

Handle concurrent API calls during token refresh to prevent race conditions and failures

Secure Token Storage in React Native

Properly storing authentication tokens is critical for maintaining the security of your React Native application. Unlike web applications with HttpOnly cookies, mobile apps have access to secure storage mechanisms specifically designed for sensitive data like credentials and tokens.

Understanding Storage Options

React Native AsyncStorage provides a persistent, unencrypted key-value store for application data. While AsyncStorage is convenient for token storage, it is not inherently secure--the data is stored in plain text in the application sandbox and can be accessed by anyone with root access to the device or a backup of the application data.

Platform-Specific Secure Storage (iOS Keychain, Android Keystore) provides hardware-backed encryption for sensitive credentials. On iOS, the Keychain Services API encrypts data using hardware-backed keys protected by the device passcode. On Android, the Android Keystore system uses hardware security modules on capable devices. Libraries like react-native-keychain abstract these platform-specific APIs, providing a unified interface for secure credential storage.

Production Storage Strategy

The recommended approach for production applications is to store the refresh token in secure device storage (Keychain/Keystore) while keeping the access token in AsyncStorage for convenience, since access tokens expire quickly and their theft window is limited. This balance provides strong security for the critical refresh token while maintaining the fast access speed needed for frequently-used access tokens.

For applications with stringent security requirements, store both tokens securely and accept the small performance cost of secure storage access in exchange for improved security. The additional milliseconds spent retrieving tokens from secure storage are negligible compared to network request times.

Your token storage implementation should include methods for retrieving tokens, saving new tokens, and clearing tokens on logout. A well-designed token service abstracts these storage details away from the rest of your application, allowing you to change storage mechanisms without affecting authentication logic throughout the codebase. This separation of concerns is essential for maintainable code that integrates cleanly with your cloud infrastructure.

// Example token storage service
const tokenService = {
 getAccessToken: () => AsyncStorage.getItem('accessToken'),
 setAccessToken: (token) => AsyncStorage.setItem('accessToken', token),
 getRefreshToken: () => secureStorage.getRefreshToken(),
 setRefreshToken: (token) => secureStorage.setRefreshToken(token),
 clearTokens: () => {
 AsyncStorage.removeItem('accessToken');
 secureStorage.removeRefreshToken();
 }
}

This architecture ensures that even if a device is compromised, the refresh token remains protected by hardware-backed encryption, limiting the impact of any potential breach.

Setting Up Axios for Token Management

Creating a well-configured Axios instance is the foundation of your authentication system. Rather than using Axios globally throughout your application, creating a dedicated Axios instance with custom configuration ensures consistent behavior and centralizes your authentication and error handling logic.

The advantages of creating a dedicated Axios instance extend beyond configuration convenience. You can add interceptors specifically to this instance without affecting other Axios usage in your application. If your application needs to communicate with multiple APIs with different authentication requirements, you can create multiple Axios instances, each with its own interceptor configuration. This modularity makes your code easier to maintain and test.

The Axios instance should be configured with a base URL that points to your API server, eliminating the need to repeat the full URL for each request. A reasonable timeout configuration (such as 60,000 milliseconds) prevents the application from hanging indefinitely on slow or failing network requests, improving the perceived responsiveness of your application. By creating this instance once and exporting it as a module, you ensure that every API call in your application uses the same authentication configuration automatically.

Axios Instance Configuration
1import axios from "axios";2import tokenService from "./tokenService";3 4export const BASE_URL = 'https://your-api-url.com';5 6const api = axios.create({7 baseURL: BASE_URL,8 timeout: 600009});10 11// Request interceptor will be added here12// Response interceptor will be added here13 14export default api;

Request Interceptors for Automatic Token Attachment

Request interceptors run before each HTTP request is sent, providing an opportunity to modify the request configuration or add authentication headers. In a JWT authentication system, the primary purpose of a request interceptor is to attach the current access token to the Authorization header.

The interceptor retrieves the access token from storage and adds it to the request configuration. If no token is available (which might happen before authentication or after logout), the request proceeds without an Authorization header. The server will reject such requests with a 401 response, which the application can handle appropriately.

This interceptor pattern ensures that authentication logic is applied consistently across every API request without requiring developers to manually attach tokens. If you need to change your token format or add additional headers, you only modify the interceptor rather than every API call in your codebase. The interceptor must return the modified config object to allow the request to proceed, or reject the promise to abort the request and trigger error handling. This centralized approach reduces bugs and maintenance overhead, especially as your application grows and authentication requirements evolve.

Request Interceptor for Token Attachment
1// Add this to your api instance configuration2api.interceptors.request.use(3 config => {4 const token = tokenService.getAccessToken();5 if (token) {6 config.headers['Authorization'] = `Bearer ${token}`;7 }8 return config;9 },10 error => Promise.reject(error)11);

Response Interceptors for Error Handling and Token Refresh

Response interceptors process server responses before they reach your application code, enabling centralized error handling and automatic token refresh. This is where seamless authentication happens--intercepting 401 errors, refreshing the token, and retrying the original request without visible disruption to the user.

The 401 Handling Flow

  1. Detection: Response interceptor checks each response for error status codes, specifically looking for 401 Unauthorized responses that indicate an expired or invalid access token.

  2. Loop Prevention: The interceptor checks the _retry flag on the original request config to prevent infinite retry loops. If the request has already been retried once, the error is passed through to application code.

  3. Refresh Initiation: If this is the first 401 for this request, the interceptor initiates the token refresh flow using the stored refresh token, making a request to the designated refresh endpoint.

  4. Token Update: Upon successful refresh, the new access token (and potentially new refresh token) is stored securely, and the Axios instance headers are updated with the new access token.

  5. Request Retry: The original request is retried with the new access token, and the response is returned to the application code as if no interruption occurred.

  6. Logout on Failure: If the refresh fails (refresh token expired or revoked), stored tokens are cleared and the user is redirected to the login screen, ensuring a secure fallback when re-authentication is required.

This flow ensures that users remain authenticated through natural token expiration while maintaining security when refresh tokens become invalid, all without requiring explicit handling in individual API calls.

Response Interceptor with 401 Handling
1api.interceptors.response.use(2 response => response,3 async error => {4 const { config, response } = error;5 const originalRequest = config;6 7 if (response?.status === 401 && !originalRequest._retry) {8 originalRequest._retry = true;9 10 try {11 const newToken = await refreshAccessToken();12 api.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;13 originalRequest.headers['Authorization'] = `Bearer ${newToken}`;14 return api(originalRequest);15 } catch (refreshError) {16 await handleLogout();17 return Promise.reject(refreshError);18 }19 }20 21 return Promise.reject(error);22 }23);

Implementing Production-Ready Token Refresh

A production-ready implementation must handle concurrent requests, prevent race conditions, and manage the refresh queue effectively. Without proper handling, multiple simultaneous API calls that receive 401 errors would each trigger a refresh request, leading to race conditions, potential token invalidation, and unnecessary network traffic.

The Concurrent Request Problem

When multiple API calls are in flight and the access token expires, all requests receive 401 errors simultaneously. Without proper handling, each request would trigger a refresh, creating multiple competing refresh requests that could invalidate each other's tokens or cause authentication failures.

The Queue Solution

The queue pattern solves this by using an isRefreshing flag to track when a refresh is in progress. The first 401 error triggers the refresh and sets the flag. Subsequent requests add their retry callbacks to a queue instead of triggering additional refreshes. When the refresh completes, all queued callbacks are invoked with the new token, and each request is retried automatically.

The subscribeTokenRefresh function adds callbacks to the queue, while onRefreshed invokes all queued callbacks with the new token and clears the queue. This ensures that exactly one refresh occurs regardless of how many requests fail with 401 errors, and all affected requests retry successfully with the new token.

Complete Token Refresh Implementation
1let isRefreshing = false;2let failedQueue = [];3 4api.interceptors.response.use(5 response => response,6 err => {7 const { config, response } = err;8 const originalRequest = config;9 10 if (response?.status === 401 && !originalRequest._retry) {11 if (!isRefreshing) {12 isRefreshing = true;13 14 return authApi.refreshToken()15 .then(({ access_token, refresh_token }) => {16 tokenService.setAccessToken(access_token);17 tokenService.setRefreshToken(refresh_token);18 api.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;19 isRefreshing = false;20 onRefreshed(access_token);21 22 return Promise.all(23 failedQueue.map(cb => cb(access_token))24 ).then(() => api(originalRequest));25 })26 .catch(error => {27 isRefreshing = false;28 tokenService.clearTokens();29 navigationRef.navigate('Login');30 return Promise.reject(error);31 });32 }33 34 originalRequest._retry = true;35 return new Promise((resolve, reject) => {36 subscribeTokenRefresh(token => {37 originalRequest.headers['Authorization'] = `Bearer ${token}`;38 resolve(api(originalRequest));39 });40 });41 }42 43 return Promise.reject(err);44 }45);

Complete Implementation Example

Putting all the pieces together, here is a complete JWT authentication module for React Native. The implementation consists of three main files working in concert: the token service handling secure storage, the authentication API module managing login and refresh operations, and the main Axios instance with interceptors managing request authentication and automatic refresh.

File Structure

src/
 services/
 api/
 index.ts // Main Axios instance with interceptors
 tokenService.ts // Token storage operations
 authApi.ts // Authentication API calls

The token service abstracts storage operations, keeping authentication logic clean. The authApi module provides a clear interface for authentication operations. The main API instance centralizes all interceptor logic, ensuring consistent behavior across your application.

tokenService.ts - Secure Token Storage
1import AsyncStorage from '@react-native-async-storage/async-storage';2import * as Keychain from 'react-native-keychain';3 4const tokenService = {5 getAccessToken: async () => {6 return AsyncStorage.getItem('accessToken');7 },8 setAccessToken: async (token) => {9 await AsyncStorage.setItem('accessToken', token);10 },11 getRefreshToken: async () => {12 try {13 const credentials = await Keychain.getGenericPassword({ service: 'refreshToken' });14 return credentials ? credentials.password : null;15 } catch (error) {16 return null;17 }18 },19 setRefreshToken: async (token) => {20 await Keychain.setGenericPassword('refreshToken', token, { service: 'refreshToken' });21 },22 clearTokens: async () => {23 await AsyncStorage.removeItem('accessToken');24 await Keychain.resetGenericPassword({ service: 'refreshToken' });25 }26};27 28export default tokenService;
authApi.ts - Authentication API Calls
1import api from './index';2import tokenService from './tokenService';3 4export const authApi = {5 login: async (credentials) => {6 const response = await api.post('/auth/login', credentials);7 const { access_token, refresh_token } = response.data;8 await tokenService.setAccessToken(access_token);9 await tokenService.setRefreshToken(refresh_token);10 return response.data;11 },12 13 refreshToken: async () => {14 const refreshToken = await tokenService.getRefreshToken();15 const response = await api.post('/auth/refresh', { refreshToken });16 const { access_token, refresh_token } = response.data;17 await tokenService.setAccessToken(access_token);18 await tokenService.setRefreshToken(refresh_token);19 return response.data;20 },21 22 logout: async () => {23 await api.post('/auth/logout');24 await tokenService.clearTokens();25 }26};

Best Practices for Production Implementations

Implementing JWT authentication in React Native requires attention to several security and usability considerations that distinguish production-ready implementations from basic examples.

Security Best Practices

Token Rotation: Each refresh should issue a new refresh token, invalidating the previous one. This limits the impact of token theft to a single use window. Some applications also implement refresh token binding, associating refresh tokens with specific device identifiers to prevent token theft from being useful on different devices.

Secure Storage: Always use Keychain/Keystore for refresh tokens, not plain AsyncStorage. Access tokens can be stored in AsyncStorage since they expire quickly, but for high-security applications, store both tokens securely.

Short-Lived Access Tokens: Limit access token lifespan to 15-60 minutes to minimize the impact of token theft. Balance security with user experience--too short and users experience frequent interruptions.

HTTPS Only: Ensure all API communication occurs over encrypted connections. Implement certificate validation and consider certificate pinning for high-security applications.

Usability Best Practices

Silent Refresh: Implement proactive token refresh before expiration to avoid interruptions. Monitor token age and trigger refresh when the token is close to expiring.

Graceful Degradation: Handle network errors without breaking the UI. Implement appropriate retry logic with exponential backoff for transient failures.

Clear Error Messages: Help users understand when re-authentication is needed. Differentiate between expired sessions (automatically redirect to login) and permission errors (show access denied).

Session Timeout: Implement appropriate session management policies based on your security requirements. Consider allowing longer sessions for low-risk operations while requiring re-authentication for sensitive actions.

Production Checklist

Token Rotation

Refresh tokens are rotated on each refresh to limit theft impact

Secure Storage

Refresh tokens stored in Keychain/Keystore, not plain AsyncStorage

Queue Management

Concurrent requests handled correctly during token refresh

Error Recovery

Graceful handling of refresh failures with logout redirect

Network Resilience

Appropriate retry logic and timeout handling

Comprehensive Testing

Tests cover normal flows, error scenarios, and edge cases

Common Pitfalls and How to Avoid Them

Insecure Token Storage

Problem: Storing refresh tokens in plain AsyncStorage exposes them to extraction by malware or through device backups. On rooted devices or through improper backup configurations, these tokens can be accessed by unauthorized applications.

Solution: Always use platform-specific secure storage (Keychain/Keystore) for refresh tokens. The react-native-keychain library provides a simple API for storing credentials securely on both iOS and Android. For access tokens, consider your threat model--AsyncStorage may be acceptable for short-lived tokens, but secure storage is safer for high-security applications.

Race Conditions in Refresh

Problem: Multiple simultaneous 401 errors trigger multiple refresh requests, causing race conditions that can invalidate tokens or create inconsistent application state. Without proper handling, every API call that receives a 401 would independently initiate a refresh.

Solution: Use the isRefreshing flag and request queue pattern described earlier. The first 401 triggers the refresh and sets the flag. Subsequent requests add to the queue rather than triggering additional refreshes. When the refresh completes, all queued requests retry with the new token.

Improper Error Handling

Problem: Failing to distinguish between different 401 causes leads to broken user experiences. A 401 might mean an expired token (refresh needed), an invalid token (refresh needed), or a revoked refresh token (re-authentication required). Treating all 401s the same can cause infinite loops or inappropriate behavior.

Solution: Your server should provide enough information for the client to determine the appropriate action. Include specific error codes in 401 responses that indicate whether the refresh token is expired versus revoked. Handle each case appropriately--refresh for expired tokens, redirect to login for revoked tokens.

Header Update Oversights

Problem: Forgetting to update Authorization headers after refresh causes subsequent requests to fail. The Axios instance's default headers must be updated, queued requests must receive the new token, and any cached references to the old token must be refreshed.

Solution: Update both stored tokens and the Axios instance's defaults.headers.common['Authorization'] in the refresh success handler. When using the queue pattern, pass the new token to each queued callback so they can update their request configuration before retrying.

Network Error Handling

Problem: Treating network failures during refresh as authentication failures can log out users unnecessarily. Network timeouts or temporary connectivity issues should not invalidate sessions.

Solution: Distinguish between authentication errors (401 with valid response from server) and network errors (no response, timeout, DNS failure). Implement retry logic for network errors with appropriate limits, and only redirect to login when the server explicitly indicates the session is invalid.

Frequently Asked Questions

Conclusion

Implementing JWT authentication with Axios interceptors in React Native creates a robust, maintainable authentication layer that protects your API communications while providing a seamless user experience. The interceptor pattern centralizes authentication logic, eliminating code duplication and ensuring consistent behavior across your application.

The key components of a production-ready implementation include secure token storage using platform-specific APIs, a dedicated Axios instance with request and response interceptors, automatic token refresh with proper concurrency handling, and graceful error handling that redirects users to login when necessary.

By following the patterns and best practices outlined in this guide, you can build a React Native application with enterprise-grade authentication that scales reliably and protects user credentials effectively. The initial investment in a well-designed authentication system pays dividends in reduced maintenance costs, improved security posture, and better user experience throughout your application's lifecycle.

For organizations looking to implement secure authentication in their React Native applications, our backend development services provide expert guidance on authentication architecture, API design, and security best practices that align with your business requirements.

Need Help Implementing Secure Authentication?

Our backend development team specializes in building secure, scalable authentication systems for React Native applications.

Sources

  1. LogRocket: React Native JWT Authentication Using Axios Interceptors - Comprehensive guide covering JWT authentication flow with Axios interceptors
  2. CashFree: Axios Interceptors JWT Refresh for React Native - Detailed coverage of JWT refresh token flow in React Native apps
  3. SolutionSquares: React Native JWT Authentication Guide - Step-by-step implementation guide for secure token management