Search code examples
javascriptaxiosjwtrefresh-token

How to avoid multiple token refresh requests when making simultaneous API requests with an expired token


API request using JWT is implemented in flask and Vue.js. The JWT is stored in a cookie, and the server validates the JWT for each request.

If the token has expired, a 401 error will be returned. f you receive a 401 error, refresh the token as in the code below, The original API request is made again. The following code is common to all requests.

http.interceptors.response.use((response) => {
    return response;
}, error => {
    if (error.config && error.response && error.response.status === 401 && !error.config._retry) {
        error.config._retry = true;
        http
            .post(
                "/token/refresh",
                {},
                {
                    withCredentials: true,
                    headers: {
                        "X-CSRF-TOKEN": Vue.$cookies.get("csrf_refresh_token")
                    }
                }
            )
            .then(res => {
                if (res.status == 200) {
                    const config = error.config;
                    config.headers["X-CSRF-TOKEN"] = Vue.$cookies.get("csrf_access_token");
                    return Axios.request(error.config);
                }
            })
            .catch(error => {

            });
    }
    return Promise.reject(error);
});

When making multiple API requests at the same time with the token expired Uselessly refreshing the token. For example, requests A, B, and C are executed almost simultaneously. Since 401 is returned with each request, Each interceptor will refresh the token.

There is no real harm, but I don't think it's a good way. There is a good way to solve this.

My idea is to first make an API request to validate the token expiration, This method is to make requests A, B, and C after verification and refresh are completed. Because cookies are HttpOnly, the expiration date cannot be verified on the client side (JavaScript).

Sorry in poor english...


Solution

  • What you'll need to do is maintain some state outside the interceptor. Something that says

    Hold up, I'm in the middle of getting a new token.

    This is best done by keeping a reference to a Promise. That way, the first 401 interceptor can create the promise, then all other requests can wait for it.

    let refreshTokenPromise // this holds any in-progress token refresh requests
    
    // I just moved this logic into its own function
    const getRefreshToken = () => http.post('/token/refresh', {}, {
      withCredentials: true,
      headers: { 'X-CSRF-TOKEN': Vue.$cookies.get('csrf_refresh_token') }
    }).then(() => Vue.$cookies.get('csrf_access_token'))
    
    http.interceptors.response.use(r => r, error => {
      if (error.config && error.response && error.response.status === 401) {
        if (!refreshTokenPromise) { // check for an existing in-progress request
          // if nothing is in-progress, start a new refresh token request
          refreshTokenPromise = getRefreshToken().then(token => {
            refreshTokenPromise = null // clear state
            return token // resolve with the new token
          })
        }
    
        return refreshTokenPromise.then(token => {
          error.config.headers['X-CSRF-TOKEN'] = token
          return http.request(error.config)
        })
      }
      return Promise.reject(error)
    })