Intercept and Decode JWT Claims in Axios

Axios request interceptors provide a built-in mechanism to capture outgoing HTTP requests before they are transmitted over the network. By attaching a request interceptor to an Axios instance, you can retrieve a JSON Web Token (JWT) from storage, decode its payload claims to inspect metadata such as expiration times or user permissions, handle token refresh logic if necessary, and attach the valid token to the Authorization header.

How Axios Request Interceptors Work

Axios executes middleware-like functions known as interceptors prior to dispatching a network request. Using axios.interceptors.request.use(), you can access and mutate the request configuration object (AxiosRequestConfig).

When processing JWTs, this hook serves as the optimal gatekeeper to:

  1. Retrieve the serialized token from memory or persistent storage.
  2. Decode the Base64Url-encoded payload without executing a full cryptographic verification (which is handled by the server).
  3. Validate client-side claims, such as checking if the token has expired (exp).
  4. Attach the formatted Bearer token to the Authorization header.

Decoding JWT Claims

A JWT consists of three parts separated by dots: Header, Payload, and Signature. The payload contains the claims. Decoding can be achieved using a standard Base64 decoding function or lightweight libraries like jwt-decode.

Pure JavaScript Decoding Function

function decodeJwtPayload(token) {
  try {
    const base64Url = token.split('.')[1];
    const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    const jsonPayload = decodeURIComponent(
      atob(base64)
        .split('')
        .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
        .join('')
    );
    return JSON.parse(jsonPayload);
  } catch (error) {
    return null;
  }
}

Implementing the Interceptor

Below is the complete implementation of an Axios instance configured to decode and validate JWT claims before request dispatch:

import axios from 'axios';

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

apiClient.interceptors.request.use(
  async (config) => {
    const token = localStorage.getItem('access_token');

    if (!token) {
      return config;
    }

    const claims = decodeJwtPayload(token);

    if (claims) {
      const currentTime = Math.floor(Date.now() / 1000);

      // Check if token has expired based on the 'exp' claim
      if (claims.exp && claims.exp < currentTime) {
        // Handle token expiration: Refresh token logic or redirect
        const newToken = await refreshAuthToken();
        config.headers.Authorization = `Bearer ${newToken}`;
        return config;
      }

      // Check custom claims or roles if necessary
      if (claims.requiresMfa) {
        config.headers['X-MFA-Required'] = 'true';
      }
    }

    // Attach token to Authorization header
    config.headers.Authorization = `Bearer ${token}`;
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

async function refreshAuthToken() {
  // Logic to call refresh endpoint and return the new access token
  const refreshToken = localStorage.getItem('refresh_token');
  const response = await axios.post('https://api.example.com/auth/refresh', {
    refreshToken,
  });
  const newToken = response.data.accessToken;
  localStorage.setItem('access_token', newToken);
  return newToken;
}

export default apiClient;

Key Considerations