DEV Community

Cover image for How to Refresh Json Web Tokens (JWT) using Axios Interceptors
Francisco Mendes
Francisco Mendes

Posted on

How to Refresh Json Web Tokens (JWT) using Axios Interceptors

Axios is a very popular http client in the community responsible for making http requests to third party services. And in the case of today, it will be used in two scenarios, the first is to make http requests that do not require any kind of authentication and the second is to refresh the token that is sent in the headers.

Introduction

In a very quick and general way, axios interceptors are functions that are invoked whenever an http request is made with the axios instance being used. These functions are widely used to refresh our application's tokens, in order to allow the user to continue using the application without having to log in consecutively.

Prerequisites

Before going further, you need:

  • Node
  • NPM

Also, it is expected that you already have a knowledge of how to consume an api and you have already used axios in the past.

Getting Started

Install dependencies

In the project you are currently working on, install the following dependencies:

npm install axios mem
Enter fullscreen mode Exit fullscreen mode

After executing the command, we will have the following dependencies:

  • axios - http client
  • mem - performs the memorization of a function

With our dependencies installed we can move on to the next step.

Configure Axios instances

As was said at the beginning of the article, we will have two instances of axios, the first one will be our public instance:

// @/src/common/axiosPublic.js
import axios from "axios";

export const axiosPublic = axios.create({
  baseURL: "http://localhost:3333/api",
  headers: {
    "Content-Type": "application/json",
  },
});
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, the public instance is very basic and will be used to make http requests whose endpoints do not need any authentication (such as sign in, sign up, etc).

With our public instance configured, we can now move on to creating the role responsible for updating our token. As you may have guessed, let's import the mem and our public axios instance:

// @/src/common/refreshToken.js
import mem from "mem";

import { axiosPublic } from "./axiosPublic";

// ...
Enter fullscreen mode Exit fullscreen mode

Then we can create a function like this (this function can change according to your backend logic):

// @/src/common/refreshToken.js
import mem from "mem";

import { axiosPublic } from "./axiosPublic";

const refreshTokenFn = async () => {
  const session = JSON.parse(localStorage.getItem("session"));

  try {
    const response = await axiosPublic.post("/user/refresh", {
      refreshToken: session?.refreshToken,
    });

    const { session } = response.data;

    if (!session?.accessToken) {
      localStorage.removeItem("session");
      localStorage.removeItem("user");
    }

    localStorage.setItem("session", JSON.stringify(session));

    return session;
  } catch (error) {
    localStorage.removeItem("session");
    localStorage.removeItem("user");
  }
};

// ...
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, first we will fetch our current session from our storage, then the current refresh token is sent to update it. If the session property does not appear in the response body or an error occurs, the user is logged out.

Otherwise, the session we have in storage is updated with the current session (which contains the respective access and refresh token).

Now this function is memoized, so that it is cached for about ten seconds:

// @/src/common/refreshToken.js
import mem from "mem";

import { axiosPublic } from "./axiosPublic";

const refreshTokenFn = async () => {
  const session = JSON.parse(localStorage.getItem("session"));

  try {
    const response = await axiosPublic.post("/user/refresh", {
      refreshToken: session?.refreshToken,
    });

    const { session } = response.data;

    if (!session?.accessToken) {
      localStorage.removeItem("session");
      localStorage.removeItem("user");
    }

    localStorage.setItem("session", JSON.stringify(session));

    return session;
  } catch (error) {
    localStorage.removeItem("session");
    localStorage.removeItem("user");
  }
};

const maxAge = 10000;

export const memoizedRefreshToken = mem(refreshTokenFn, {
  maxAge,
});
Enter fullscreen mode Exit fullscreen mode

Why is memoization done? The answer is very simple, if twenty http requests are made and all of them get a 401 (Unauthorized), we don't want the token to be refreshed twenty times.

When we memoize the function, the first time it is invoked, the http request is made, but from then on, its response will always be the same because it will be cached for ten seconds (and without making the other nineteen http requests).

With the public instance of axios configured and the refresh token function memorized, we can move on to the next step.

Configure the Interceptors

Unlike what we did before, now we are going to create an axios instance that will be responsible for making http requests to endpoints that require authentication.

First let's import the axios and our memoized refresh token function:

// @/src/common/axiosPrivate.js
import axios from "axios";

import { memoizedRefreshToken } from "./refreshToken";

axios.defaults.baseURL = "http://localhost:3333/api";

// ...
Enter fullscreen mode Exit fullscreen mode

First, let's configure the interceptor that will be invoked before the http request is made. This one will be responsible for adding the access token in the headers.

// @/src/common/axiosPrivate.js
import axios from "axios";

import { memoizedRefreshToken } from "./refreshToken";

axios.defaults.baseURL = "http://localhost:3333/api";

axios.interceptors.request.use(
  async (config) => {
    const session = JSON.parse(localStorage.getItem("session"));

    if (session?.accessToken) {
      config.headers = {
        ...config.headers,
        authorization: `Bearer ${session?.accessToken}`,
      };
    }

    return config;
  },
  (error) => Promise.reject(error)
);

// ...
Enter fullscreen mode Exit fullscreen mode

Now we can configure our interceptor that will be responsible for intercepting the response if our api sends a 401. And if it is sent, let's refresh the token and try again to make the http request but this time with the new access token.

// @/src/common/axiosPrivate.js
import axios from "axios";

import { memoizedRefreshToken } from "./refreshToken";

axios.defaults.baseURL = "http://localhost:3333/api";

axios.interceptors.request.use(
  async (config) => {
    const session = JSON.parse(localStorage.getItem("session"));

    if (session?.accessToken) {
      config.headers = {
        ...config.headers,
        authorization: `Bearer ${session?.accessToken}`,
      };
    }

    return config;
  },
  (error) => Promise.reject(error)
);

axios.interceptors.response.use(
  (response) => response,
  async (error) => {
    const config = error?.config;

    if (error?.response?.status === 401 && !config?.sent) {
      config.sent = true;

      const result = await memoizedRefreshToken();

      if (result?.accessToken) {
        config.headers = {
          ...config.headers,
          authorization: `Bearer ${result?.accessToken}`,
        };
      }

      return axios(config);
    }
    return Promise.reject(error);
  }
);

export const axiosPrivate = axios;
Enter fullscreen mode Exit fullscreen mode

Conclusion

In today's article, a simple example of how the refresh token is done in an application was given. As you may have noticed in the article, localStorage was used, but with some code adjustments it is possible to adapt this same example to use cookies. And this strategy can be easily used with frontend frameworks like React, Vue, Svelte, Solid, etc.

I hope you liked it and I'll see you next time.

Discussion (6)

Collapse
unthrottled profile image
Alex Simons

Had a question, does mem handle cache stampeding?

I know that libraries like dataloader where designed to handle such cases where multiple request for the same object happen at the same time. It will coalesce all the calls, make one call to the function, and supply all the batched calls with the value returned from the function.

Was just wondering if mem does the same thing. Thanks!

Collapse
franciscomendes10866 profile image
Francisco Mendes Author

To tell you the truth, I don't know, I like to use mem whenever I'm going to invoke a function in a consecutive way and I want to cache it for a certain period of time.

It's a simple approach, but it's been working properly so far, but thanks a lot for the question!

Collapse
ahmad_butt_faa7e5cc876ea7 profile image
Ahmad

great and consise article, thanks!

looking at the memoization, any reason for choosing this value for maxAge? just wondering

const maxAge = 10000;

export const memoizedRefreshToken = mem(refreshTokenFn, {
maxAge,
});

Collapse
franciscomendes10866 profile image
Francisco Mendes Author • Edited on

Thanks for the feedback! Regarding the value of maxAge, in the article I put ten seconds but I usually use a value close to twenty. This is because http requests may have a timeout (maybe not the best word to describe it) which may be longer than maxAge value.

Collapse
sainig profile image
Gaurav Saini

Very nice and to the point article.
I’m also using a similar interceptor, but after getting the new access token, I was struggling to replay the failed request. This was a great help, thanks 👍

Collapse
franciscomendes10866 profile image
Francisco Mendes Author • Edited on

Glad to know!