Table of Contents
Developing a secure and smooth app, especially for payments, is crucial. In creating the Cashfree Payments app with React Native, we focused on top-notch security and easy user experience. Key to our strategy is handling JSON Web Tokens (JWT) efficiently for user sessions.
We used Axios, a JavaScript library, to refresh JWTs seamlessly through interceptors. This method improves security and user experience by updating tokens automatically, without user interruption.
In this guide, we share insights on integrating JWT refresh in React Native apps with Axios, providing valuable tips for developers on enhancing authentication processes easily.
JWT Refresh Token Flow in React Native Apps

-
- The client initiates the authentication process by sending a POST request with their username and password to the server’s /api/auth/signin endpoint.
-
- Upon successful authentication, the server generates and returns a JSON Web Token (JWT) along with a `refreshToken`, user information, and roles.
-
- The client adds the JWT to the Authorisation header of every subsequent request to access protected resources.
-
- If the server detects an expired JWT, indicated by a TokenExpiredError, it notifies the client.
-
- The client requests a new JWT using the `refreshToken` by sending a POST request to /api/auth/refreshtoken.
-
- The server verifies the `refreshToken` and, upon successful validation, issues a new JWT and possibly a new `refreshToken`.
-
- This process ensures secure and efficient user authentication and session management in the application.
How to Set up Axios for Token Management in React Native
Axios is initialised with axios.create(), providing a tailored Axios instance with specific configuration options to enhance HTTP request handling. Key configurations include:
-
- Setting the baseURL to Config.BASE_URL, which simplifies API interactions by automatically appending the base URL to each endpoint path. This eliminates the need for repetitive full URL specifications throughout the codebase.
-
- Defining a timeout limit of 60,000 milliseconds to ensure application responsiveness. This timeout acts as a safeguard against prolonged server response times, aborting requests that exceed this limit. This enhances user experience by preventing the application from becoming unresponsive during server delays.
Creating a dedicated Axios instance like this offers several advantages:
-
- Consistency: Every request made using this instance will automatically inherit these configurations, ensuring consistency across your application.
-
- Maintainability: Centralising the Axios configuration makes the codebase easier to maintain. If you need to change the base URL or adjust the timeout, you only need to do it in one place.
-
- Flexibility: If you need to request a different API or with different settings, you can easily create another instance with its configurations.
Enhancing JWT Security with Axios Interceptors
Interceptors are like special helpers that work with HTTP requests and responses. They step in either right before we send a request to a server, or just after we receive a response, but before the rest of our app gets to work with that request/response. This allows us to check our HTTP messages in specific ways, making it easier to manage things like adding security tokens to requests or consistently handling errors. This includes adjustments to configurations, headers, and payload data, thereby providing a versatile toolset for enforcing consistency, security, and data integrity across all network interactions facilitated by Axios.
There are two types of interceptors: request interceptors and response interceptors
Request Interceptors
The request interceptor is invoked immediately before the dispatch of a request, offering a last-minute opportunity to refine or augment the outgoing request. Common use cases include appending authentication credentials, serialising request payloads, or dynamically adjusting headers based on contextual parameters.
-
- Before any request is made, the interceptor function retrieves the access token using storage.getAccessToken().
-
- If the token exists, it is added to the Authorization field in the headers of the outgoing request. This standard practice ensures that each request is authenticated automatically, without the need to manually attach the token for every HTTP call. By returning Promise.reject(error), the error can be caught and handled in the context where the HTTP request was made
axios.interceptors.request.use(
(config) => {
// Get token and add it to header "Authorization" from secure storgage
const token = storage.getAccessToken()
if (token) {
config.headers.Authorization = token;
}
return config;
},
(error) => Promise.reject(error)
);
Response Interceptors
Activated upon the receipt of a response, these interceptors allow for preemptive processing of the returned data. Whether it’s for global error handling, data transformation, or extracting and forwarding pertinent information, response interceptors streamline post-response processing, enhancing the robustness and resilience of the application’s data-handling capabilities
-
- For successful responses (status codes within the 2xx range), the interceptor simply passes the response through
-
- For responses with error status codes (outside the 2xx range), the interceptor captures these errors. This is particularly useful for centralised error handling, like logging errors or redirecting the user to an error page. By returning Promise.reject(error), the error can be further caught and handled in the specific context where the HTTP request was made.
axios.interceptors.response.use(
(response) => {
// Any status code from range of 2xx
// Do something with response data
return response;
},
(error) => {
// Any status codes outside range of 2xx
// Do something with response error
return Promise.reject(error);
});
Handling a 401 Response in Axios Interceptors
To effectively manage a 401 response, our approach involves a six-step method that begins by intercepting all server responses to identify the 401 status code
1. Intercepting Responses: Axios interceptors allow you to run your code or handle errors before the response is returned to the code that made the request. You can define a response interceptor that checks the HTTP status code of every response from the server.
2. Detecting 401 Status Code: When the interceptor detects a response with a 401 status code, it recognises this as an authentication error, typically due to an expired or invalid token.
3. Initiating Token Refresh: Upon detecting a 401 response, instead of immediately returning an error to the requester, the interceptor can initiate a process to refresh the token. This is done by calling a function or API endpoint dedicated to refreshing tokens. This mechanism usually involves sending a refresh token (a longer-lived token securely stored on the client) to the server to obtain a new access token
4. Queueing Incoming Requests: While the token is being refreshed, any new requests initiated by the client are queued or paused. This is because they would also fail with a 401 status if the expired or invalid token is still being used
5. Resuming Requests with New Token: Once a new access token is obtained, it is used to update the Authorization header of the original request and any queued requests. These requests can then be retried automatically without requiring the user to manually re-authenticate.
6. Handling Refresh Token Failure: If the refresh token process fails (for example, the refresh token has expired or is no longer valid), the user must be sent to a login page or asked to re-authenticate themselves manually. As a result, the user experience stays smooth and safe.
Code Example for JWT Token Refresh in Axios
request.ts
import axios from "axios";
import tokenService from "./tokenService";
import { authApi } from "./authApi";
import { navigationRef } from './helper';
export const BASE_URL = 'http://your-api-url.com';
let isRefreshing = false;
let failedQueue = [];
const request = axios.create({
baseURL: BASE_URL,
timeout: 60000
});
// Add the auth token to every request
request.interceptors.request.use(
config => {
const token = tokenService.getAccessToken();
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
function subscribeTokenRefresh(cb) {
failedQueue.push(cb);
}
function onRefreshed(token) {
failedQueue.forEach(cb => cb(token));
failedQueue = [];
}
request.interceptors.response.use(
response => response,
err => {
const { config, response } = err;
const originalRequest = config;
if (response && response.status === 401 && !originalRequest._retry) {
if (!isRefreshing) {
isRefreshing = true;
return authApi.refreshToken()
.then(({ access_token, refresh_token }) => {
tokenService.setAccessToken(access_token);
tokenService.setRefreshToken(refresh_token);
request.defaults.headers.common['Authorization'] = `Bearer${access_token}`;
isRefreshing = false;
onRefreshed(access_token);
return Promise.all(failedQueue.map(cb => cb(access_token))).then(() => {
return axios(originalRequest);
});
})
.catch(error => {
isRefreshing = false;
tokenService.clearTokens();
navigationRef.navigate('Login');
return Promise.reject(error);
})
.finally(() => failedQueue = []);
}
originalRequest._retry = true;
return new Promise((resolve, reject) => {
subscribeTokenRefresh(token => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
resolve(axios(originalRequest));
});
});
}
return Promise.reject(err);
}
);
export default request;
helper.ts
/**
* Provides a service for managing authentication tokens using AsyncStorage in a React Native application.
* Includes methods to get, set, and clear both access and refresh tokens, facilitating secure and efficient
* authentication token management throughout the app.
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
const tokenService = {
getAccessToken: () => {
return AsyncStorage.getItem('accessToken');
},
setAccessToken: (token) => {
AsyncStorage.setItem('accessToken', token);
},
getRefreshToken: () => {
return AsyncStorage.getItem('refreshToken');
},
setRefreshToken: (token) => {
AsyncStorage.setItem('refreshToken', token);
},
clearTokens: () => {
AsyncStorage.removeItem('accessToken');
AsyncStorage.removeItem('refreshToken');
}
}
export default tokenService;
authApi.ts
/**
* Defines API calls related to refresh token
* The refreshToken method sends a request to the server with the current refresh token to obtain a new access token
*/
import axios from 'axios';
import { BASE_URL } from './request';
const authApi = {
refreshToken: async () => {
const refreshToken = tokenService.getRefreshToken();
try {
const response = await axios.post(`${BASE_URL}/refresh-token`,{refreshToken });
return response.data;
} catch (error) {
console.error('Error refreshing token:', error);
throw error;
}
}
};