Handling token-based authentication and refreshing token in Axios, Axios interceptors

Photo by Tierra Mallorca on Unsplash

If you are lazy like me and don’t want to read the whole article, here is GitHub gist containing cheatsheet for a refreshing token using Axios interceptors — https://gist.github.com/alitoshmatov/9ba256a7981da4f397072f0679f4344e

When I was a beginner in the field I had no idea how authentication works on the client-side and it took me long before I grasp the concept of authentication. So I am here to do my best to ease your journey into the token-based authentication concept on the client-side.

Let’s refresh our knowledge on how token-based authentication works. So basically when a client requests to log in with valid credentials, the backend server responds with a unique access token that stores some user and session-related data in an encrypted format. After successful authentication, each time client makes a request, this token is also sent with request data, so the backend server can validate and identify which user is making a request and/or check if they have permission to do so.

In general, we can say that this token is acting as an access key, and the backend server won’t open it is doors to the wrong keys.

However access token is short-lived meaning that it will be valid for minutes or rarely for hours. After the specified time backend server won’t accept the token, and the token expires. Well, you might say have a user to re-authenticate every time their token expires!? Fortunately, no, they don’t have to. When a user logs in, alongside the access token backend server also sends a refresh token. Refresh token, as its name suggests, is used to update/refresh regular token when it expires. There would be an API endpoint that takes refresh token in request data and responds with a new access token. So when the client-side makes a request and if the backend server responds with a message saying token expired, client-side should automatically make a request for a new access token, and after retrieving a new access token, it should also make the original request again. You can check out this stackoverflow thread to explore more the purpose of this cycle, as explaining it deserves its own article.

If you are reading this article, I assume that you know how to make basic requests using Axios, also feel free to check out the official documentation.

axios.post('https://example.com/api/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

Basically, this is how it is done. But we probably would benefit by initializing Axios instance with preferred configurations.

const customRequest = axios.create({
baseURL: 'https://example.com/api/',
headers: {'Request-Origin': 'website'}
});

As you can see we have initialized our Axios instance with base URL, and header, now each request made using request instance is going to have this base URL and header by default. Keep in mind, we can easily override this configuration.

customRequest.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

Before jumping into handling tokens, let’s tackle the login process. This process is very straightforward. we should just make a request to login API, get all necessary data and store it somewhere, possibly in local storage or session storage to persist data between sessions.

const handleLogin = (email, password)=>{
customRequest.post("/login", {email: email, password:password})
.then(response=>{
const token = response.data.token;
const refreshToken = response.data.refreshToken;
localStorage.setItem("accessToken", token);
localStorage.setItem("refreshToken", refreshToken);
const user = response.data.user;
//handle user
})
.catch(e=>console.log(e)
}

As you can see above, we are retrieving user data, and both tokens from the backend server, and storing tokens in local storage to persist them between sessions. Based on your needs you can handle user data however you want.

Now we have our Axios instance and have retrieved tokens from a server, let’s see how we can send authorization token in the header of our every request. Note that we can only retrieve tokens once the user logs in, and we can’t tell for sure that token always persists in local storage or somewhere else, meaning that our token won’t always be available. Thus, we can’t initialize our Axios instance with the token at the beginning. So we need to use something called interceptors.

Interceptors are a kind of middleware which we can modify requests before they are fired, and responses before they are made available on client-side.

customRequest.interceptors.request.use(config=> {
// Do something before request is sent
return config;
});

In the above code we are applying interceptor to requests, getting config object which contains all data related to current request which is going to be fired. Feel free to explore this object as it has a lot of interesting data on it. Now we can modify the config object, and attach an additional header to carry our token.

customRequest.interceptors.request.use(config=> {
const accessToken = localStorage.getItem("accessToken");

//checking if accessToken exists
if(accessToken){
config.headers["Authorization"]=accessToken;
}

return config;
});

Above, first, we are retrieving accessToken from local storage, if accessToken exists in local storage we are assigning it to header named Authorization, and returning modified config object so making the request can carry on. Note that, header name which you assign token value may be different in your project, clarify it with the backend team. Now we are done with sending tokens in the header of each request if the token exists.

So how refreshing token should work: at any point, our access token could expire, so when we make a request with an expired token, the backend server responds with a 401(Unauthorized) status code. When we get this response, we try to refresh our access token and retry the original request. Note that the response for the expired token might be different, clarify with the backend team. Let’s apply response interceptor:

customRequest.interceptors.response.use( (config)=> config);

Like request interceptor, response interceptor also provides us with config object which has all necessary data including the response. When our request is successful we want just to return the original config object. But when we get the status code of 401 our response throws an error. Axios interceptors accept the second function which is a callback for handling errors.

customRequest.interceptors.response.use(
(response) => response,
async (error) => {
//extracting response and config objects
const { response, config } = error;
//checking if error is Aunothorized error
if (response.status === 401) {
let refreshToken = localStorage.getItem("refreshToken");
if (refreshToken) {
//if refresh token exists in local storage proceed
try {
//try refreshing token
const data = await customRequest.post("/token/refresh/", {
refresh: refreshToken,
});
let accessToken = data.data.accessToken;
if (accessToken) {
//if request is successful and token exists in response
//store it in local storage
localStorage.setItem("token", accessToken);
//with new token retry original request
config.headers["Authorization"]=accessToken;
return customRequest(config);
}
}
catch (e) {
console.log(e);
}
}
}
//if none above worked clear local storage and log user out
logout();
return error;
});

This portion might be a bit large to understand, let’s see chunk by chunk what we are doing:

  • First of all, we are extracting the response object which contains our response with error code, and the config object which has all necessary configurations related to the current request.
  • Then, we are checking if our error code is 401 which is the response for expired tokens. If so we are proceeding, else it goes to the end of the function and logs the user out.
  • Now, let’s retrieve our refresh token from local storage and check if it really exists. If we have a refresh token, we can send a request for a new access token with the refresh token.
  • If our request for a new token fails it goes to the end of the function and ends the cycle by logging the user out. After successfully getting a new access token, we update the token in local storage and retry our original request with the updated token.
  • Note that, if any of our if conditions fail or try-catch block catches error we go to the end of the function and log the user out.

You can add any additional logic or modify this code based on your needs, but most time the flow is the same.

I made a small cheatsheet for handling access and refresh tokens using Axios interceptors, check it out here. Also, would love to hear suggestions for any modifications and improvements. — https://gist.github.com/alitoshmatov/9ba256a7981da4f397072f0679f4344e

React dev.