JWT Authentication using Axios interceptors
Tags: react, authenticatio, axios, interceptors
Published: 2022-11-09 11:32:34

Hello 👋! In this blog, I will show you how I usually implement axios interceptors when I build an app that requires authentication. In this case, we will use React, but in can easily be ported to another framework (Most of the time I did it in Vue).

We will use the backend from this [blog post](https://dev.to/mihaiandrei97/jwt-authentication-using-prisma-and-express-37nk).

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:
```sh
npx degit mihaiandrei97/blog-refresh-token-interceptor react-auth
```

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.

## 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.

For the user state management, we will use zustand (because we need to access the tokens inside axios interceptors, and that can't be done with React Context because the state is not accessible outside components).

I like to keep all of my api calls inside a 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](https://axios-http.com/docs/interceptors), 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 interceptor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6ujxiqrofh9cge9hbz9j.png)


Let's create a file called `services/createAxiosClient.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.

```js
import axios from 'axios';

export function createAxiosClient({
  options,
  getCurrentAccessToken,
  getCurrentRefreshToken,
  refreshTokenUrl,
  logout,
  setRefreshedTokens,
}) {
  const client = axios.create(options);

  client.interceptors.request.use(
    (config) => {
      if (config.authorization !== false) {
        const token = getCurrentAccessToken();
        if (token) {
          config.headers.Authorization = "Bearer " + token;
        }
      }
      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );

 return client;

}
```

The createAxiosClient takes the following arguments:
- options: the options that are passed to the axios instance, example: baseUrl, timeout etc.
- getCurrentAccessToken: a function that provides the accessToken from the store.
- getCurrentRefreshToken: a function that provides the accessToken from the store.
- refreshTokenUrl: the url endpoint that should be called when the access token is expired.
- logout: a function that performs the logout logic when the refreshToken called failed( ex: cleanup storage / redirect to /login)
- setRefreshedTokens: a function that sets the tokens in store/localStorage.

We could move the logic directly into `createAxiosClient` instead of passing those helpers functions, but with this approach, we can easily move the axiosInstance to a different state management (ex: Redux), or to a different framework (Vue / Svelte).

The request interceptor that we just wrote does a simple thing. Checks if the specific request requires authentication, and if it does, it calls the method: `getCurrentAccessToken`, and adds the token to the header in order to be passed along to the server.
With this approach, we no longer have to manually specify the access token for each request that we write. We just need to use this axios instance.

### Step 2 - Create the services

Let's create the file where we will put all of our logic that creates the axios instance.

Create a file in the `services` directory called `axiosClient.js`.

```js
import { createAxiosClient } from "./createAxiosClient";
import { useAuthStore } from "../src/stores/authStore";

const REFRESH_TOKEN_URL = 'http://localhost:5000/api/v1/auth/refreshToken'
const BASE_URL = 'http://localhost:5000/api/v1/'

function getCurrentAccessToken() {
    // this is how you access the zustand store outside of React.
    return useAuthStore.getState().accessToken
}

function getCurrentRefreshToken() {
    // this is how you access the zustand store outside of React.
    return useAuthStore.getState().refreshToken
}


function setRefreshedTokens(tokens){
    console.log('set tokens...')
}

async function logout(){
    console.log('logout...')
}

export const client = createAxiosClient({
    options: {
        baseURL: BASE_URL,
        timeout: 300000,
        headers: {
            'Content-Type': 'application/json',
        }
    },
    getCurrentAccessToken,
    getCurrentRefreshToken,
    refreshTokenUrl: REFRESH_TOKEN_URL,
    logout,
    setRefreshedTokens
})
```
In this file, we call the `createAxiosClient` function and we export the client in order to use it in our services. We have also defined the URL's (BASE_URL and REFRESH_TOKEN_URL), and we used [zustand](https://github.com/pmndrs/zustand) in order to get the tokens from the global state.

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

```js
import { client } from "./axiosClient";

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

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

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

```
Here, we imported the client instance, and we use it to make requests like we would normally do with the `axios` keyword. 
If you notice, for the login/endpoint, we specified `authorization: false`, because those endpoints are public. If we omit it, then by default it will fire the `getCurrentAccessToken` function.

Now, let's change the axios calls with the one from services.
Let's go to the `Login` page and in the action, change the following code:
```js
const url =
      type === "register"
        ? "http://localhost:5000/api/v1/auth/register"
        : "http://localhost:5000/api/v1/auth/login";
const { data } = await axios.post(url, {
      email,
      password,
    });
```
with:
```js
const response = type === "register" ? await register({email, password}) : await login({email, password});
const { accessToken, refreshToken } = response.data;
```

Now, if you try to register/login, it should work like before. The interceptor doesn't really have a point for this endpoints, but you could say it is better structured now.

Next, let's go to the `Home` page. 

There, instead of:
```js
useEffect(() => {
    if (isLoggedIn)
      axios
        .get("http://localhost:5000/api/v1/users/profile", {
          headers: {
            Authorization: `Bearer ${accessToken}`,
          },
        })
        .then(({ data }) => {
          setProfile(data);
        })
        .catch((error) => {
          if (error.response.data.message === "TokenExpiredError") {
            logout();
          }
        });
  }, [isLoggedIn, accessToken]);
```

We can add:
```js
 useEffect(() => {
    if (isLoggedIn) {
      getProfile().then(({data}) => {
        setProfile(data);
      }).catch(error => {
        console.error(error);
      })
   }
  }, [isLoggedIn]);
```

It looks much cleaner now. You can see that we do not manually set the Authentication header anymore since the axios interceptor does that for us.
Also, probably you noticed that we are not checking anymore for the "TokenExpiredError". We will do that in the response interceptor soon.

### Step 3 - Create Axios Interceptor for response

Here, things get a bit more complicated, but I will try to explain it the as good as I can :D. If you have questions, please add them in the comments.

The final code for `createAxiosClient.js` is:
```js
import axios from "axios";

let failedQueue = [];
let isRefreshing = false;

const processQueue = (error) => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve();
    }
  });

  failedQueue = [];
};

export function createAxiosClient({
  options,
  getCurrentAccessToken,
  getCurrentRefreshToken,
  refreshTokenUrl,
  logout,
  setRefreshedTokens,
}) {
  const client = axios.create(options);

  client.interceptors.request.use(
    (config) => {
      if (config.authorization !== false) {
        const token = getCurrentAccessToken();
        if (token) {
          config.headers.Authorization = "Bearer " + token;
        }
      }
      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );

  client.interceptors.response.use(
    (response) => {
      // Any status code that lie within the range of 2xx cause this function to trigger
      // Do something with response data
      return response;
    },
    (error) => {
      const originalRequest = error.config;
      // In "axios": "^1.1.3" there is an issue with headers, and this is the workaround.
      originalRequest.headers = JSON.parse(
        JSON.stringify(originalRequest.headers || {})
      );
      const refreshToken = getCurrentRefreshToken();

      // If error, process all the requests in the queue and logout the user.
      const handleError = (error) => {
        processQueue(error);
        logout();
        return Promise.reject(error);
      };

      // Refresh token conditions
      if (
        refreshToken &&
        error.response?.status === 401 &&
        error.response.data.message === "TokenExpiredError" &&
        originalRequest?.url !== refreshTokenUrl &&
        originalRequest?._retry !== true
      ) {
        
        if (isRefreshing) {
          return new Promise(function (resolve, reject) {
            failedQueue.push({ resolve, reject });
          })
            .then(() => {
              return client(originalRequest);
            })
            .catch((err) => {
              return Promise.reject(err);
            });
        }
        isRefreshing = true;
        originalRequest._retry = true;
        return client
          .post(refreshTokenUrl, {
            refreshToken: refreshToken,
          })
          .then((res) => {
            const tokens = {
              accessToken: res.data?.accessToken,
              refreshToken: res.data?.refreshToken,
            };
            setRefreshedTokens(tokens);
            processQueue(null);

            return client(originalRequest);
          }, handleError)
          .finally(() => {
            isRefreshing = false;
          });
      }

      // Refresh token missing or expired => logout user...
      if (
        error.response?.status === 401 &&
        error.response?.data?.message === "TokenExpiredError"
      ) {
        return handleError(error);
      }

      // Any status codes that falls outside the range of 2xx cause this function to trigger
      // Do something with response error
      return Promise.reject(error);
    }
  );

  return client;
}


```

The workflow can be seen in this diagram:

![Axios response interceptor](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/svtmyil0pgzyveqh9l06.png)


Now, let's take it step by step.

#### The queue implementation:

We create an array `failedQueue` 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

First, we get the token via `getCurrentRefreshToken` function that was passed to `createAxiosClient`.
Second, we need to check the following:
- Do we have a refresh token?
- Did we receive a response with status 401 and the message TokenExpiredError?
- Is the url different from the Refresh token url? (because we do not want to trigger it if the refresh token responds with an expired message)
- Is this the first time we try this request? (originalRequest?._retry !== true)

If all this conditions are true, then we can go further. If the `isRefreshing` flag is already true, it means we triggered the refresh with an early call, so we just need to add the current call to the queue. If not, then this is the first call, so we change the flag to true, and we proceed with the refreshToken call to the back-end. If the call is successful, we call 'setRefreshTokens' that was passed to the client, we process the queue(start all the requests from the queue with the new tokens), and we retry the original request that triggered the refresh.

If the refresh token was missing, or it was expired, we just process the queue as an error and we logout the user.

Now, the last thing we need to do write the logic for setRefreshTokens and logout.

Go to `services/axiosClient.js` and change them like this:
```js
function setRefreshTokens(tokens){
    console.log('set refresh tokens...')
    const login = useAuthStore.getState().login
    login(tokens)
}

async function logout(){
    console.log('logout...')
    const logout = useAuthStore.getState().logout
    logout()
}
```

### 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 🎉🎉🎉.

If you have any questions, feel free to reach up in the comments section.