DEV Community

Cover image for JWT Authentication using Axios interceptors
Mihai-Adrian Andrei
Mihai-Adrian Andrei

Posted on • Edited on • Originally published at mihai-andrei.com

JWT Authentication using Axios interceptors

Hello 👋! In this blog, I will show you to create an axios client using interceptors to use with an app that requires JWT authentication. In this case, we will use React, but in can easily be ported to another framework.

We will use the backend from this blog post.

For this, I've created a starter repository for us to focus only on the refresh token part. You can clone it with this command:

npx degit mihaiandrei97/blog-refresh-token-interceptor react-auth
Enter fullscreen mode Exit fullscreen mode

Inside it, you will see two folders:

  • react-auth-start: here is the code that you will be using for this project.
  • react-auth-finished: here is the final code, if you missed something and you need to check it.

Or if you want to see the final code, you can check it here.

Project explanation

The application has 2 pages:

  • A Login page, with a form where the user can register/login and after that we save the tokens in localStorage.
  • A Home page where we display the user profile if he is logged in.

I like to keep all of my api calls inside a file/folder called services. With this approach, I can see all the calls used in the app.

Step 1 - Create Axios Interceptor for request

As a first step, let's define the axios interceptors. You can read more about them here, but as a simple explanation, we will use them to execute some code before we make a request, or after we receive a response.

This is what we will implement:

Axios request logic

Let's create a file inside lib called app/lib/axiosClient.js:

Here, we will define a function that will create our axios instance. We will use that instance everywhere in the app, instead of axios. If we do that, for each request/response, our interceptors will be executed.

import axios from "axios";

class AxiosInterceptor {
  constructor(instanceConfig = {}) {
    // Initialize Axios instance with provided configuration
    this.axiosInstance = axios.create({
      ...instanceConfig,
    });

    // Add request interceptor
    this.axiosInstance.interceptors.request.use(
      (config) => {
        const accessToken = this.getAccessToken();
        if (accessToken) {
          config.headers.Authorization = `Bearer ${accessToken}`;
        }
        return config;
      },
      (error) => Promise.reject(error),
    );

    // Bind instance methods for convenience
    this.get = this.axiosInstance.get.bind(this.axiosInstance);
    this.post = this.axiosInstance.post.bind(this.axiosInstance);
    this.put = this.axiosInstance.put.bind(this.axiosInstance);
    this.delete = this.axiosInstance.delete.bind(this.axiosInstance);
  }

  getAccessToken() {
    return localStorage.getItem("accessToken");
  }
}

// Export a pre-configured instance of AxiosInterceptor
export const client = new AxiosInterceptor({
  baseURL: "http://localhost:5000/api/v1",
});
Enter fullscreen mode Exit fullscreen mode

In this file, we define the class AxiosInterceptor and we create an instance of axios with the provided configuration. We also add a request interceptor that will add the Authorization header with the accessToken from localStorage.

Step 2 - Create the services

Now, let's create the app/lib/services.js file, where we would store all of our api calls.

import { client } from "./axiosClient";

export function register({ email, password }) {
  return client.post("auth/register", { email, password });
}

export function login({ email, password }) {
  return client.post("auth/login", { email, password });
}

export function getProfile() {
  return client.get("/users/profile");
}
Enter fullscreen mode Exit fullscreen mode

Here, we imported the client instance, and we use it to make requests like we would normally do with the axios keyword.

And now we can use these functions in our components. Let's go to the Login component and add the login/register functionality. Let's add the clientAction that is going to be called when the form is submitted.

export async function clientAction({ request }) {
  try {
    const formData = await request.formData();
    const type = formData.get("type");
    const email = formData.get("email");
    const password = formData.get("password");

    const response =
      type === "register"
        ? await register({ email, password })
        : await login({ email, password });
    const { accessToken, refreshToken } = response.data;
    localStorage.setItem("accessToken", accessToken);
    localStorage.setItem("refreshToken", refreshToken);
    return redirect("/");
  } catch (error) {
    return {
      error: error?.response?.data?.message || error.message,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

In this function, we get the form data and we call the register or login function from the services.js file. After that, we save the tokens in localStorage and redirect the user to the home page.
Next, let's go to the Home page.

There, let's add the getProfile function that will be called when the component mounts.

export async function clientLoader() {
  try {
    const accessToken = localStorage.getItem("accessToken");
    if (!accessToken) {
      return redirect("/login");
    }

    const { data } = await getProfile();

    return { profile: data };
  } catch {
    localStorage.removeItem("accessToken");
    return redirect("/login");
  }
}
Enter fullscreen mode Exit fullscreen mode

In this function, we check if the user is logged in. If not, we redirect him to the login page. If he is logged in, we call the getProfile function and we return the user profile.

We can also add a Logout button and display the profile.

export async function clientAction() {
  localStorage.removeItem("accessToken");
  localStorage.removeItem("refreshToken");
  return null;
}

export default function Home(props) {
  return (
    <div className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-3xl mx-auto bg-white shadow-md rounded-lg overflow-hidden">
        <div className="px-6 py-8">
          <h1 className="text-3xl font-bold text-center text-gray-900 mb-8">
            Welcome to React Auth!
          </h1>

          <div className="bg-gray-50 rounded-lg p-4 mb-8 overflow-auto">
            <pre className="text-sm text-gray-700">
              <code>{JSON.stringify(props.loaderData, null, 2)}</code>
            </pre>
          </div>

          <div className="flex flex-col sm:flex-row justify-center items-center space-y-4 sm:space-y-0 sm:space-x-4">
            <Form method="post">
              <button
                type="submit"
                className="w-full sm:w-auto bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 ease-in-out"
              >
                Logout
              </button>
            </Form>
          </div>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 3 - Create Axios Interceptor for response

Here, things get a bit more complicated, but don't worry, I will explain everything.

The final code for axios client will look like this:

import axios from "axios";

/**
 * A class to intercept Axios requests and handle token-based authentication.
 * It retries unauthorized (401) requests after refreshing the access token.
 */
class AxiosInterceptor {
  /**
   * Creates an AxiosInterceptor instance.
   * @param {object} [instanceConfig={}] - Configuration for the Axios instance.
   */
  constructor(instanceConfig = {}) {
    this.isRefreshing = false; // Tracks if a token refresh is in progress
    this.refreshSubscribers = []; // Queue for requests waiting for the token to refresh

    // Initialize Axios instance with provided configuration
    this.axiosInstance = axios.create({
      ...instanceConfig,
    });

    // Add request interceptor
    this.axiosInstance.interceptors.request.use(
      (config) => {
        const accessToken = this.getAccessToken();
        if (accessToken) {
          config.headers.Authorization = `Bearer ${accessToken}`;
        }
        return config;
      },
      (error) => Promise.reject(error),
    );

    // Add response interceptor
    this.axiosInstance.interceptors.response.use(
      (response) => response,
      async (error) => {
        const originalRequest = error.config;

        if (
          error.response &&
          error.response.status === 401 &&
          error.response.data.message === "TokenExpiredError" &&
          !originalRequest._retry
        ) {
          if (!this.isRefreshing) {
            this.isRefreshing = true;

            try {
              const newTokens = await this.refreshTokens();
              this.setAccessToken(newTokens.accessToken);
              this.setRefreshToken(newTokens.refreshToken);

              this.refreshSubscribers.forEach((callback) =>
                callback(newTokens.accessToken),
              );
              this.refreshSubscribers = [];

              return this.axiosInstance(originalRequest);
            } catch (refreshError) {
              this.refreshSubscribers = []; // Clear the queue in case of failure
              this.setAccessToken("");
              this.setRefreshToken("");
              return Promise.reject(refreshError);
            } finally {
              this.isRefreshing = false;
            }
          }

          return new Promise((resolve) => {
            this.refreshSubscribers.push((newAccessToken) => {
              originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
              originalRequest._retry = true;
              resolve(this.axiosInstance(originalRequest));
            });
          });
        }

        return Promise.reject(error);
      },
    );

    // Bind instance methods for convenience
    /**
     * Makes a GET request.
     * @type {import('axios').AxiosInstance['get']}
     */
    this.get = this.axiosInstance.get.bind(this.axiosInstance);

    /**
     * Makes a POST request.
     * @type {import('axios').AxiosInstance['post']}
     */
    this.post = this.axiosInstance.post.bind(this.axiosInstance);

    /**
     * Makes a PUT request.
     * @type {import('axios').AxiosInstance['put']}
     */
    this.put = this.axiosInstance.put.bind(this.axiosInstance);

    /**
     * Makes a DELETE request.
     * @type {import('axios').AxiosInstance['delete']}
     */

    this.delete = this.axiosInstance.delete.bind(this.axiosInstance);
  }

  /**
   * Retrieves the current access token from localStorage.
   * @returns {string|null} The access token or null if not available.
   */
  getAccessToken() {
    return localStorage.getItem("accessToken");
  }

  /**
   * Stores a new access token in localStorage.
   * @param {string} token - The new access token.
   */
  setAccessToken(token) {
    localStorage.setItem("accessToken", token);
  }

  /**
   * Retrieves the current refresh token from localStorage.
   * @returns {string|null} The refresh token or null if not available.
   */
  getRefreshToken() {
    return localStorage.getItem("refreshToken");
  }

  /**
   * Stores a new refresh token in localStorage.
   * @param {string} token - The new refresh token.
   */
  setRefreshToken(token) {
    localStorage.setItem("refreshToken", token);
  }

  /**
   * Refreshes the authentication tokens by calling the refresh endpoint.
   * @returns {Promise<{ accessToken: string, refreshToken: string }>} The new access and refresh tokens.
   * @throws {Error} If the token refresh request fails.
   */
  async refreshTokens() {
    const refreshToken = this.getRefreshToken();
    if (!refreshToken) {
      throw new Error("No refresh token available");
    }

    const response = await this.axiosInstance.post("/auth/refreshToken", {
      refreshToken,
    });
    return response.data; // Expecting { accessToken: string, refreshToken: string }
  }
}

// Export a pre-configured instance of AxiosInterceptor
export const client = new AxiosInterceptor({
  baseURL: "http://localhost:5000/api/v1",
});
Enter fullscreen mode Exit fullscreen mode

The workflow can be seen in this diagram:

Axios Interceptors workflow

Now, let's take it step by step.

The queue implementation

We need to keep track of the requests that are waiting for the token to refresh. We will use an array called refreshSubscribers for this, where we put the requests that failed at the same time when we tried to refresh the token. When is this usefull? When we have multiple calls in paralel. If we make 4 requests, we probably don't want each of them to trigger a refreshToken. In that case, only the first one triggers it, and the other ones are put in the queue, and retried after the refresh is finished.

Refresh token logic

  • When we receive a 401 error, we check if the error is caused by the fact the token is expired;
  • If it is, we check if we are already refreshing the token;
    • If we are, we add the request to the queue and we return a promise that will be resolved when the token is refreshed;
    • If not, we set the isRefreshing flag to true and we call the refreshTokens function;
    • If the refresh is successful, we save the new tokens in localStorage, we call the requests from the queue and we clear the queue;
    • If the refresh fails, we clear the queue and we remove the tokens from localStorage.

Conclusion

And that's it. Now, by using axios interceptors, your app should automatically add the access token to the header and also handle the refresh token silently, in order to keep the user authenticated 🎉🎉🎉.

Top comments (9)

Collapse
 
arthur_bella_a52b8afbdcd4 profile image
Arthur Bella

Hi great article 👍.

Please can you share the link to a github repository (if any)?
The thing, I try to implement this using reduxjs and as you use zustand I wanted to know how you define the store. For instance when you do use AuthStore.getState().login I don't know if login is a reducer action or not so I'm a little bit confuse.

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Hello! For sure. I thought it is understandable by default that degit goes to github for this:
npx degit mihaiandrei97/blog-refresh-token-interceptor react-auth

Here is the repo: github.com/mihaiandrei97/blog-refr...

Collapse
 
talinthedev profile image
Talin Sharma

I switched from React to Vue recently and I really don't want to go back so is there a simple way to use most of this code here in Vue without rewriting major portions of it? I'm fine with minor rewrites, that's expected but I don't want to basically rewrite the whole thing myself. Thanks for this and the backend article! They helped a ton!

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

The axios part is the same. The useEffects could be changed into created or watchers in vue

Collapse
 
talinthedev profile image
Talin Sharma

Okay, thanks!

Collapse
 
sk0le profile image
Amel Islamovic

Hello, I just want to ask question, I tried converting this to typescript and I don't have any luck.
This is error that I get:

Argument of type '{ authorization: boolean; }' is not assignable to parameter of type 'AxiosRequestConfig<{ email: any; password: any; }>'.
Enter fullscreen mode Exit fullscreen mode

And this happens when I call client.post()
Any help man.

Collapse
 
sipkoepp profile image
sipkoepp • Edited

I ran into the same issue. You need to overwrite the AxiosRequestConfig. Just place this anywhere in your code. I placed it on top of the createAxiosClient function.

// Add a new property to AxiosRequestConfig interface
declare module "axios" {
  export interface AxiosRequestConfig {
    authorization?: boolean;
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Thanks! Good to know

Collapse
 
mihaiandrei97 profile image
Mihai-Adrian Andrei

Hello. Can you create a repo and share the url So I can check?