Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 421
nayuta shiba Avatar asked Mar 19 '20 01:03

nayuta shiba


People also ask

How do I protect my refresh token?

Protecting your refresh tokens Concretely, refresh tokens exposed to the browser should be protected with Refresh Token Rotation (RTR). In a nutshell, RTR makes refresh tokens only valid for one-time use. Each time a refresh token is used, the security token service issues a new access token and a new refresh token.

Can I use refresh token multiple times?

A refresh token doesn't expire until the new access token obtained from it was executed in an API call. When the new access token has been used, the refresh token that was used to obtain it automatically expires.

How do you handle expired tokens?

So in summary when authorization is successful you need to issue two token ACCESS_TOKEN and REFRESH_TOKEN. When ACCESS_TOKEN expires you need to call another api with REFRESH_TOKEN to get new ACCESS_TOKEN. The client application can get a new access token as long as the refresh token is valid and unexpired.

What if refresh token expired?

When an access token expires, a refresh token is used to get a new access token and it also returns a new refresh token. Now if this new access token expires & a new/updated refresh token is used to get the next access token, it will also receive a newer refresh token.


1 Answers

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)
})
like image 186
Phil Avatar answered Oct 18 '22 00:10

Phil