Refresh Token Using Axios in React

Axios interceptors enable pre-handling of requests or responses. Here, we'll see how to refresh authentication tokens using them

Understanding Authentication Tokens and Token Refresh

Authentication tokens, like JSON Web Tokens (JWT), validate users in web apps and expire after a set time, requiring users to refresh them for uninterrupted access to protected resources.

Token refresh exchanges an expired token for a new one using a long-lived refresh token, eliminating the need for users to log in again by sending a request with the refresh token to obtain new access tokens.

Steps

  • Install axios via npm or yarn.

  • We create an Axios instance named api with a base URL.

  • We define a refreshToken function responsible for obtaining a new token from the server by making a POST request to /refresh-token.

  • We use a request interceptor to add the token to outgoing requests.

  • We use a response interceptor to handle token expiration errors (status code 401).

  • If a token expiration error occurs, we attempt to refresh the token and retry the original request with the new token.

Now, let's set up Axios and define our interceptor functions for token refresh. Here's a basic setup in TypeScript:

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';

const api = axios.create({
  baseURL: 'https://api.example.com',
});

// Function to refresh token
async function refreshToken(): Promise<string | null> {
  try {
    const response = await axios.post('/refresh-token');
    const newToken = response.data.accessToken;

    // Save the new token to local storage
    localStorage.setItem('accessToken', newToken);

    return newToken;
  } catch (error) {
    console.error('Error refreshing token:', error);
    return null;
  }
}

// Request interceptor for adding the token to requests
api.interceptors.request.use(
  async (config: AxiosRequestConfig) => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// Response interceptor for token refresh
api.interceptors.response.use(
  (response: AxiosResponse) => {
    return response;
  },
  async (error) => {
    const originalRequest = error.config;
    if (error.response && error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      const newToken = await refreshToken();
      if (newToken) {
        // Retry the original request with the new token
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        return axios(originalRequest);
      }
    }
    return Promise.reject(error);
  }
);

export default api;

Explanation of _retry Flag

The _retry flag prevents an infinite loop of token refresh attempts for a single request. It ensures that a request is retried only once after a token refresh, avoiding potential issues with continuously failing requests.

Why Refresh Function Uses axios Instead of api Instance

The refreshToken function bypasses the api instance declared at the start of the file due to the following reasons:

  1. Interception Avoidance: Using axios directly in the refreshToken function prevents interception by Axios interceptors, ensuring a clean request for fetching a new access token.

  2. Circular Dependency Prevention: Directly using axios avoids circular dependencies between the api instance and the refreshToken function, which could lead to module loading issues.

This approach maintains a clear separation of concerns, allowing the token refresh mechanism to function smoothly without interference from interceptors or dependencies.

Now, for making API calls hooked up to the refresh flow, you can simply import the API object and use it how you would normally use the axios instance:

import api from './api';

async function fetchData() {
  try {
    const response = await api.get('/data');
    console.log(response.data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

fetchData();

In this example, the fetchData function utilizes the api instance to fetch data from the /data endpoint. If an error occurs during the request, it is caught and logged.

Instead of creating a separate instance we can also use the global instance while initialising the interceptors, in which case we can make API calls with axios itself.

Why Use Local Storage or Cookies to Store Tokens?

It's better to store tokens (both access and refresh tokens) in local storage or cookies due to security reasons:

  1. Protection against XSS attacks: Storing tokens in local storage or cookies helps mitigate the risk of cross-site scripting (XSS) attacks. Access to tokens stored in local storage or cookies is limited to the domain from which they originated, reducing the likelihood of them being accessed by malicious scripts injected from other domains.

  2. Prevention of CSRF attacks: Cookies can be configured with the HttpOnly flag, which prevents them from being accessed by client-side scripts. This mitigates the risk of cross-site request forgery (CSRF) attacks, where an attacker tricks a user's browser into making unauthorized requests by exploiting their active session.

  3. Persistent authentication: Tokens stored in local storage or cookies persist across browser sessions, providing a seamless user experience. Users remain authenticated even after refreshing the page or closing the browser, reducing the need for frequent logins.

Did you find this article valuable?

Support Vaisakh Np by becoming a sponsor. Any amount is appreciated!