nuxt-community / auth-module

Zero-boilerplate authentication support for Nuxt 2
https://auth.nuxtjs.org
MIT License
1.93k stars 925 forks source link

Axios Interceptor Refresh Token called multiple times on parallel axios request #1743

Open tokidoki11 opened 2 years ago

tokidoki11 commented 2 years ago

Version

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

Nuxt configuration

mode:

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

florianPat commented 2 years ago

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

thomasv commented 2 years ago

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.

sadeghi-aa commented 2 years ago

What do people think about this solution?

Regards, Flo

Where should I put this function?

trandaison commented 2 years ago

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

trandaison commented 2 years ago

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