How to implement refresh tokens JWT in NestJS with React Part-2

Implement refresh token with JWT in React App using Axios

Hello everyone, I’m back with the continuation of the previous content this time we will implement the refresh token solution previously developed in our React application. Are you ready? Let’s go!

Understanding how client-side refresh token works

First of all, we need to know how the refresh token strategy will work in our SPA application.

  • After the user logs into our application, our API will return an object containing the AccessToken, RefreshToken, and UserInfo.
  • AccessToken and RefreshToken will be saved in LocalStorage and at each request for a new endpoint with access protection only for authenticated users, for example, the user data display route, AccessToken will be added in the header of this request.
  • Using the Axios library to perform these requests, we have a method called interceptor. This method allows us to intercept our request or response before our data is processed, making it possible to check if our AccessToken has expired and sent a new request to gain access to our request route that we want to access.

Let’s implement refresh token in our application

It’s time to put into practice our solution described above, first, we need to declare a variable that creates an instance of Axios with the custom configuration for our request and response.

Interceptors.ts

import axios, {
  AxiosError,
  AxiosInstance,
  AxiosRequestConfig,
  AxiosResponse,
} from "axios";
import Router from "next/router";

const API_URL = process.env.NEXT_PUBLIC_ENDPOINT_AUTH;

const onRequest = (config: AxiosRequestConfig): AxiosRequestConfig => {
  const token = JSON.parse(localStorage.getItem("token"));
  config.headers["Authorization"] = `Bearer ${token.access_token}`;

  return config;
};

const onRequestError = (error: AxiosError): Promise<AxiosError> => {
  return Promise.reject(error);
};

const onResponse = (response: AxiosResponse): AxiosResponse => {
  return response;
};

const onResponseError = async (error: AxiosError): Promise<AxiosError> => {
  if (error.response) {
    // Access Token was expired
    if (
      error.response.status === 401 &&
      error.response.data.message === "jwt expired"
    ) {
      const storedToken = JSON.parse(localStorage.getItem("token"));

      try {
        const rs = await axios.post(`${API_URL}/auth/refresh`, {
          refresh_token: storedToken.refresh_token,
        });

        const { token, user } = rs.data;

        localStorage.setItem("token", JSON.stringify(token));
        localStorage.setItem("user", JSON.stringify(user));

        return;
      } catch (_error) {
        return Promise.reject(_error);
      }
    }
  }
  return Promise.reject(error);
};

export const setupInterceptorsTo = (
  axiosInstance: AxiosInstance
): AxiosInstance => {
  axiosInstance.interceptors.request.use(onRequest, onRequestError);
  axiosInstance.interceptors.response.use(onResponse, onResponseError);
  return axiosInstance;
};

In the file above we created the functions to intercept the requests and their responses as well as the errors of each of these actions. We export the setupInterceptorsTo function to apply the callbacks on the Axios instance to make it reusable. We also handle in case of a response with status 401 and the token expired message, thus triggering a request to our endpoint to refresh the token and update our access token saved in localStorage.

Now we need to use the function created in our interceptors.ts to apply the interceptor before updating the service with the Axios interceptors, for that we will create an api.ts file.

import axios from "axios";
import { setupInterceptorsTo } from "@services/interceptors";

const api = setupInterceptorsTo(
  axios.create({
    baseURL: process.env.NEXT_PUBLIC_ENDPOINT_AUTH,
    headers: {
      "Content-Type": "application/json",
    },
  })
);

export default api;

Whooooo!!! the configuration of the interceptors is done, now we can use the setupInterceptorsTo() function. In this case, we are configuring the interceptors for the Axios instance as global to be used in our service.

Axios interceptors can now manipulate the header of requests sent to the server so that it is not necessary to add headers to each request, you can see below:

api.get(`${API_URL}/auth/me`)

//...
import api from "@services/api";

//...

  useEffect(() => {
    const loadUserFromLocalStorage = async () => {
      return api.get(`${API_URL}/auth/me`).then((response) => {
        if (response) {
          setUser(response.data);
          return response.data;
        }
      });
    };
    loadUserFromLocalStorage();
  }, []);

//...

In the image below we can see that our request was successful because the accessToken has not expired:

Now our access token expired and then we had status code 401 and the message that the token is expired, we can see that our strategy of generating a new token for the user and updating the expired token in localStorage worked.

After refreshed token:

Conclusion

We’ve come to the end of our saga of implementing refresh token in our application using Axios Interceptors, but don’t be discouraged as we’ll have more series with issues we encounter every day while we’re coding 😂 , until next time, and hope you leave your comment with improvements in our code.

Comments