Axios interceptor is not returning to login screen when token is expired

Tags: , ,



I’m trying to write a response interceptor for my React project but I am having some issues.

When a user gets a 401 from their original request I want to try and refresh the token and continue, but if the user gets a 401 from their original request and when trying to refresh the token it fails then redirect them to the login page.

What I have does the first bit just fine, it refreshes the token and continues with the original request, but the issue i am having is that if the refresh fails, its not redirecting the user to the login page.

I would love some input on what I am doing wrong

import axios from 'axios';
import { useRouter } from 'next/router'

const router = useRouter();

const apiInstance = axios.create({
    baseURL: process.env.API_URL
});

apiInstance.interceptors.response.use((response) => {
    return response;
}, async function (error) {
    const originalRequest = error.config;

    if (error.response.status === 401 && originalRequest.url === '/oauth/token') {
        router.push('/');
        return Promise.reject(error);
    }

    if (error.response.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;

        return axios.post(process.env.OAUTH_BASE_URL + '/oauth/token', {
            'grant_type': 'refresh_token',
            'refresh_token': localStorage.getItem('refresh_token'),
            'client_id': process.env.CLIENT_ID,
        })
        .then(res => {
            if (res.status === 200) {
                localStorage.setItem('access_token', res.access_token);
                localStorage.setItem('refresh_token', res.refresh_token);
                localStorage.setItem('expires_in', res.expires_in);

                axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('access_token');

                return apiInstance(originalRequest);
            }
        })
    }

    return Promise.reject(error);
});

export default apiInstance;

Answer

There’s a couple of errors here. First, url property is equal to the whole value of url param of axios call, so this…

originalRequest.url === '/oauth/token'

… is only true if process.env.OAUTH_BASE_URL is an empty string (and most likely it’s not). In general, it’s better to avoid checking against URLs and use flags/custom properties set on request objects (as with _retry flag).

Also, note that while apiInstance is used for regular API call, the particular call for refresh token actually avoids it:

return axios.post(process.env.OAUTH_BASE_URL + '/oauth/token', { // 
       ^^^^^^^^^^

… which means interceptors for this call are not even fired.


Here’s one possible approach to solve this. apiInstance here is the exported axios instance, and setTokens/getAccessToken/getRefreshToken are simple abstractions over mechanisms of storing/retrieving particular tokens.

apiInstance.interceptors.request.use(request => {
  if (!request._refreshToken) {
    request.headers.Authorization = 'Bearer ' + getAccessToken();
  }
  // console.log('REQUEST', request.method + ' ' + request.url);
  return request;
});

apiInstance.interceptors.response.use(
  void 0, // better skip this argument altogether
  error => {
    const originalRequest = error.config;
    if (originalRequest._refreshToken) {
      console.log('REFRESH TOKEN FAILED');
      // ... and all the things you need to do when refreshing token failed,
      // like resettting access token, and rerouting users to /login page,
      // or just sending an event for Router to process

      return Promise.reject(error);
    }

    const errorResponse = error.response;
    if (errorResponse.status !== 401) {
      return Promise.reject(error);
    }

    return apiInstance.post('/oauth/token', {
      grant_type: 'refresh_token',
      refresh_token: getRefreshToken(), 
      client_id: process.env.CLIENT_ID,
    }, {
      _refreshToken: true // custom parameter
    }).then((resp) => {
      setTokens(resp.data);
      return apiInstance(originalRequest);
    });
  }
);

There are two ideas behind this (easily testable with unit tests): first, failed refresh token requests always stop the interceptor chain (as they throw immediately), second, if ‘business-level’ API request fails, it’s always preceded with refresh-token one.

Note that this code is just a prototype to illustrate the concept here. If you expect your code to be able to issue multiple API calls at once, token refresh should actually be wrapped into a function returning single promise (to avoid subsequent refresh-token calls). If you’re going to use this in production, I strongly suggest at least considering using axios-auth-refresh instead of writing your own implementation for that.



Source: stackoverflow