4

(I've read a number of similar questions here, and most/all have said to use a different axios instance for the refresh token requests (versus the API requests). However, I'm not clear on how that would work, since I am using axios-auth-refresh for auto-refreshing the access tokens.)

I'm working on an app with a JWT-based authentication flow for back-end API requests. The general flow is working fine; upon login the user gets a long-term refresh token and short-term access token. Using the axios-auth-refresh plug-in for axios, I am able to auto-refresh the access token when it has expired.

My problem is, when the refresh token expires, I am not able to catch the error and redirect the user to re-authenticate. Nothing I've tried catches the error. The (current) code for the auto-refresh hook is:

const refreshAuth = (failed) =>
  axios({ method: "post", url: "token", skipAuthRefresh: true })
    .then(({ status, data: { success, accessToken } }) => {
      console.warn(`status=${status}`);
      if (!success) Promise.reject(failed);

      processToken(accessToken);
      // eslint-disable-next-line no-param-reassign
      failed.response.config.headers.Authorization = `Bearer ${accessToken}`;
      return Promise.resolve();
    })
    .catch((error) => console.error("%o", error));
createAuthRefreshInterceptor(axios, refreshAuth);

In cases of the refresh token being stale or missing, I see neither the status=xxx console line nor the dump of an error object in the catch() block.

The actual file this is in is on GitHub here, though it is slightly different than the working version above. Mainly, in the GH version the hook calls axios.post("token").then(...) where above I'm making a more explicit call to add the skipAuthRefresh parameter. Adding that got me more detailed error traces in the console, but I am still not catching the 401 response via the catch().

I've tried everything I can think of... anything jump out as something I'm missing?

Randy

(Edited to ensure the GitHub link points to the version of the file that has the issue.)

rjray
  • 5,525
  • 4
  • 31
  • 37

3 Answers3

3

Since posting this, I have managed to work through the problem and come up with a working solution.

The key to the solution does in fact lie in using a different axios instance for the calls to renew the refresh token. I created a second module to encapsulate a second axios instance that would not get the interceptor created by the axios-auth-refresh module. After working around some inadvertent circular-dependency issues that this initially caused, I reached a point where I could see the exception being thrown by axios when the refresh token itself is stale or missing.

(Interestingly, this led to another problem: once I recognized that the refresh token was no longer valid, I needed to log the user out and have them return to the login screen. Because the application this is in is a React application, the authentication was being handled with custom hooks, which can only be called within a component. However, I had abstracted all the API calls into a non-React module so that I could encapsulate things like the addition of the Authorization header, the base URL, etc. At that level I could not run the auth hook to get access to the logout logic. I solved this by putting a default onError handler on the query object (a react-query object) that I use for all the API calls.)

rjray
  • 5,525
  • 4
  • 31
  • 37
1

I built upon the Request class from this SO answer to refresh the token and handle the refresh failures.

Now my Request looks like this:

import axios from "axios";

import {getLocalStorageToken, logOut, refreshToken} from "./authentication";

class Request {

  ADD_AUTH_CONFIG_HEADER = 'addAuth'

  constructor() {
    this.baseURL = process.env.REACT_APP_USER_ROUTE;
    this.isRefreshing = false;
    this.failedRequests = [];
    this.axios = axios.create({
      baseURL: process.env.REACT_APP_USER_ROUTE,
      headers: {
        clientSecret: this.clientSecret,
      },
    });
    this.beforeRequest = this.beforeRequest.bind(this);
    this.onRequestFailure = this.onRequestFailure.bind(this);
    this.processQueue = this.processQueue.bind(this);
    this.axios.interceptors.request.use(this.beforeRequest);//<- Intercepting request to add token
    this.axios.interceptors.response.use(this.onRequestSuccess,
      this.onRequestFailure);// <- Intercepting 401 failures
  }

  beforeRequest(request) {
    if (request.headers[this.ADD_AUTH_CONFIG_HEADER] === true) {
      delete request.headers[this.ADD_AUTH_CONFIG_HEADER];
      const token = getLocalStorageToken();//<- replace getLocalStorageToken with your own way to retrieve your current token
      request.headers.Authorization = `Bearer ${token}`;
    }
    return request;
  }

  onRequestSuccess(response) {
    return response.data;
  }

  async onRequestFailure(err) {
    console.error('Request failed', err)
    const {response} = err;
    const originalRequest = err.config;

    if (response.status === 401 && err && originalRequest && !originalRequest.__isRetryRequest) {
      if (this.isRefreshing) {
        try {
          const token = await new Promise((resolve, reject) => {//<- Queuing new request while token is refreshing and waiting until they get resolved
            this.failedRequests.push({resolve, reject});
          });
          originalRequest.headers.Authorization = `Bearer ${token}`;
          return this.axios(originalRequest);
        } catch (e) {
          return e;
        }
      }
      this.isRefreshing = true;
      originalRequest.__isRetryRequest = true;
      console.log('Retrying request')
      console.log('Previous token', getLocalStorageToken())
      try {
        const newToken = await refreshToken()//<- replace refreshToken with your own method to get a new token (async)
        console.log('New token', newToken)
        originalRequest.headers.Authorization = `Bearer ${newToken}`;
        this.isRefreshing = false;
        this.processQueue(null, newToken);
        return this.axios(originalRequest)
      } catch (err) {
        console.error('Error refreshing the token, logging out', err);
        await logOut();//<- your logout function (clean token)
        this.processQueue(err, null);
        throw response;//<- return the response to check on component layer whether response.status === 401 and push history to log in screen
      }
    }
    throw response;
  }

  processQueue(error, token = null) {
    this.failedRequests.forEach((prom) => {
      if (error) {
        prom.reject(error);
      } else {
        prom.resolve(token);
      }
    });
    this.failedRequests = [];
  }
}

const request = new Request();

export default request;


Brahyam
  • 311
  • 2
  • 4
0

My problem is, when the refresh token expires, I am not able to catch the error and redirect the user to re-authenticate. Nothing I've tried catches the error. The (current) code for the auto-refresh hook is:

What is the return code from your api if the access token expired ?

if it is different than 401 (default) you need to configure, see exanoke 403:

createAuthRefreshInterceptor(axios, refreshAuthLogic, {
  statusCodes: [ 401, 403 ] // default: [ 401 ]
});
  • Thanks, but this wasn't the issue here (my backend is only returning 401's on authorization errors). I need to answer my own question, as I've since figured out the solution. – rjray Mar 19 '21 at 18:18