Axios with OAuth 2.0 PKCE Flow Guide

This article explains how to configure and use the Axios HTTP client to interact with APIs protected by OAuth 2.0 using the Proof Key for Code Exchange (PKCE) flow. It covers how Axios facilitates token exchange, automatically attaches Bearer tokens via request interceptors, and seamlessly handles token expiration and refreshes using response interceptors.

The Role of Axios in OAuth 2.0 PKCE

Axios does not natively implement OAuth 2.0 specifications out of the box; instead, it serves as the transport layer to execute the HTTP requests required by the PKCE flow.

The PKCE flow consists of two main phases where Axios is utilized:

  1. Token Acquisition: Sending a POST request with the authorization code and cryptographic code_verifier to the authorization server's token endpoint to receive an access_token and refresh_token.
  2. Authenticated Requests: Attaching the received access_token to subsequent requests directed at protected resource endpoints.
// Example: Exchanging the authorization code for tokens
async function exchangeCodeForTokens(code, codeVerifier) {
  const response = await axios.post('https://auth.example.com/oauth/token', {
    grant_type: 'authorization_code',
    client_id: 'your-client-id',
    code_verifier: codeVerifier,
    code: code,
    redirect_uri: 'https://yourapp.com/callback'
  }, {
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  });

  return response.data; // contains access_token, refresh_token, expires_in
}

Attaching Tokens with Request Interceptors

Once the access token is acquired, every request to a protected endpoint must include it in the Authorization header as a Bearer token. Axios request interceptors automate this by injecting the token before any HTTP request is dispatched.

import axios from 'axios';

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

apiClient.interceptors.request.use((config) => {
  const token = getStoredAccessToken(); // Retrieve from secure memory or storage

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

Handling Token Refresh with Response Interceptors

Access tokens are short-lived. When an access token expires, protected endpoints return a 401 Unauthorized HTTP status code. Axios response interceptors can catch this specific error, use the stored refresh_token to request a new access token, update the headers, and replay the original request transparently.

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    // Check if error is 401 and the request hasn't been retried yet
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const refreshToken = getStoredRefreshToken();
        
        const response = await axios.post('https://auth.example.com/oauth/token', {
          grant_type: 'refresh_token',
          client_id: 'your-client-id',
          refresh_token: refreshToken
        }, {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
        });

        const newAccessToken = response.data.access_token;
        storeNewTokens(response.data);

        // Update the authorization header and retry original request
        originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh token failed or expired; redirect to login
        logoutUser();
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

Best Practices for Axios with PKCE