auth-module icon indicating copy to clipboard operation
auth-module copied to clipboard

Axios Interceptor Refresh Token called multiple times on parallel axios request

Open tokidoki11 opened this issue 3 years ago • 5 comments

Version

module: "5.0.0-1643791578.532b3d6" nuxt: ^2.15.7

Nuxt configuration

mode:

  • [x] universal
  • [x] spa

Nuxt configuration

Reproduction

  1. in the cookie, update token expiry to 1 (making the token expired)
  2. Run axios request on parallel

What is expected?

  1. RequestToken is only run once

What is actually happening?

  1. Refresh Token runs twice image

Additional information

I have separate axios instance for calling API other than auth0

const internalAxios = $axios.create()
internalAxios.onRequest(async (config: AxiosRequestConfig): Promise<AxiosRequestConfig> => {
    const auth0Strategy = app.$auth.strategy as Auth0Scheme
    if (!auth0Strategy.token.status().valid()) {
      if (auth0Strategy.refreshToken.status().valid()) {
        await app.$auth.refreshTokens()
      } else {
        app.$auth.reset()
        app.$auth.loginWith('auth0')
      }
    }
    config.headers.authorization = decodeURI(auth0Strategy.token.get() as string)
    return config
  })

Calling refreshToken via $auth will call the refresh Token only once On further investigation calling refreshToken via $auth will trigger https://github.com/nuxt-community/auth-module/blob/c9880dc28fa13ba036f078d37ff76e1267c65d21/src/core/auth.ts#L266-L279

https://github.com/nuxt-community/auth-module/blob/c9880dc28fa13ba036f078d37ff76e1267c65d21/src/inc/refresh-controller.ts#L13-L20

But the interceptor will call this in which it doesnt check whether refresh Token is still ongoing or not https://github.com/nuxt-community/auth-module/blob/c9880dc28fa13ba036f078d37ff76e1267c65d21/src/inc/request-handler.ts#L61-L68

https://github.com/nuxt-community/auth-module/blob/c9880dc28fa13ba036f078d37ff76e1267c65d21/src/schemes/oauth2.ts#L433

Checklist

  • [ ] I have tested with the latest Nuxt version and the issue still occurs
  • [ ] I have tested with the latest module version and the issue still occurs
  • [x] I have searched the issue tracker and this issue hasn't been reported yet

tokidoki11 avatar May 10 '22 06:05 tokidoki11

Hi @tokidoki11, we face exactly the same issue. We decorated the RequestHandlers "requestInterceptor" and added a promise queue to let other requests wait on the one who initiated it:


async requestInterceptor(config) {
        // eslint-disable-next-line no-underscore-dangle,no-console
        console.log('this token', this)
        // eslint-disable-next-line no-underscore-dangle
        if (!this._needToken(config) || config.url === this.refreshEndpoint) {
            return config
        }
        const {
            valid,
            tokenExpired,
            refreshTokenExpired,
            isRefreshable,
        } = this.scheme.check(true)
        let isValid = valid
        if (refreshTokenExpired) {
            this.scheme.reset()
            throw new ExpiredAuthSessionError()
        }
        if (tokenExpired) {
            if (!isRefreshable) {
                this.scheme.reset()
                throw new ExpiredAuthSessionError()
            }
            // eslint-disable-next-line no-console
            console.log('token expired')
            if (this.promiseQueue.working) {
                // eslint-disable-next-line no-console
                console.log('waiting on the refresh')
                await this.promiseQueue.enqueue(() => new Promise((resolve) => {
                    resolve()
                }))
                return this.requestInterceptor(config)
            }
            isValid = await this.promiseQueue.enqueue(async () => {
                // eslint-disable-next-line no-console
                console.log('enqueue new refresh')
                this.scheme.refreshTokens()
                    .then(() => true)
                    .catch(() => {
                        this.scheme.reset()
                        throw new ExpiredAuthSessionError()
                    })
            })
        }
        const token = this.scheme.token.get()
        if (!isValid) {
            // eslint-disable-next-line no-underscore-dangle
            if (!token && this._requestHasAuthorizationHeader(config)) {
                throw new ExpiredAuthSessionError()
            }
            return config
        }
        // eslint-disable-next-line no-underscore-dangle
        return this._getUpdatedRequestConfig(config, token)
    }

    initializeRequestInterceptor(refreshEndpoint) {
        this.refreshEndpoint = refreshEndpoint
        this.interceptor = this.axios.interceptors.request.use(this.requestInterceptor.bind(this))
    }

What do people think about this solution?

Regards, Flo

florianPat avatar May 12 '22 09:05 florianPat

That's fine @florianPat - could you post the implementation your promise queue? Maybe thats a fix that can be implemented directly in the RequestHandler.initializeRequestInterceptor()?

Or would it be an alternative to request a new token just every time before it gets expired and not when it is "too late". But, I know, requests during token updating would have to be queued, too.

thomasv avatar May 12 '22 12:05 thomasv

What do people think about this solution?

Regards, Flo

Where should I put this function?

sadeghi-aa avatar Sep 13 '22 04:09 sadeghi-aa

I opened a PR to fix this: https://github.com/nuxt-community/auth-module/pull/1796

trandaison avatar Nov 01 '22 04:11 trandaison

This bug has been fixed in v5.0.0-1667386184.dfbbb54.

trandaison avatar Nov 03 '22 06:11 trandaison