microsoft-authentication-library-for-js: MSAL does not return the new token after an acquireSilent()

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report  
[ ] Performance issue
[ ] Feature request
[ ] Documentation issue or request
[ ] Other... Please describe:

Browser:

  • Chromium version 74 (with Brave)
  • Firefox version XX
  • IE version XX
  • Edge version XX
  • Safari version XX

Library version

Library version: 1.0.1

Current behavior

  • I’m using MSAL with VueJS
  • I use the localStorage (and not the sessionStorage)
  • I use axios to intercept outgoing requests to my API, and inject the accessToken from acquireTokenSilent() as a bearer token

When the token expires, MSAL fetches a new one (using acquireTokenSilent()), updates the localStorage accordingly, but still returns the old one. As a result, when I call my API, I have 401 errors.

  • When I refresh the page, acquireTokenSilent() returns the new token.
  • When I open a new tab from the same app, the new tab acquireTokenSilent() returns the new token. However, the old tab still uses the old token

The localStorage just after a login: image

After a token refresh: image

The new token is present (in green), while the old one is still there in red. MSAL still returns the old one when calling acquireTokenSilent()

Expected behavior

MSAL should use the new token

Minimal reproduction of the problem with instructions

The ajax interceptor:

    axios.interceptors.request.use(
      async request => {
        await setToken(request)
        return request
      },
      error => {
        return Promise.reject(error)
      }
    )

    const setToken = async (request: any) => {
      const res = await authService.getAuthResponse() as Msal.AuthResponse
      request.headers.common['Authorization'] = `Bearer ${res.accessToken}` // This fails if MSAL requested a new token
    }

The authService:

  public async getAuthResponse(): Promise<null | Msal.AuthResponse> {
    const tokenRequest = {
      scopes: this.graphScopes
    }

    try {
      return await this.userAgentApp.acquireTokenSilent(tokenRequest)
    }
    catch (e) {
      this.userAgentApp.acquireTokenRedirect(tokenRequest)
      return null
    }
  }

UPDATED WORKAROUND:

To manually retrieve the correct token:

function extractMSALToken() {
  const timestamp = Math.floor((new Date()).getTime() / 1000)
  let token = null

  for (const key of Object.keys(localStorage)) {
    if (key.includes('"authority":')) {
      const val: any = JSON.parse(localStorage.getItem(key)!)

      if (val.expiresIn) {
        // We have a (possibly expired) token

        if (val.expiresIn > timestamp && val.idToken === val.accessToken) {
          console.log(key)
          // Found the correct token
          token = val.idToken
        }
        else {
          console.log('will remove ' + key)
          // Clear old data
          localStorage.removeItem(key)
        }
      }
    }
  }
  if (token) return token
  throw new Error('No valid token found')
}

Old workaround (invalid):

    const setToken = async (request: any) => {
      // Make sure that MSAL token is up-to-date
      const res = await authService.getAuthResponse() as Msal.AuthResponse
      // DIRTY FIX : directly fetch the token from the localStorage
      request.headers.common['Authorization'] = `Bearer ${localStorage.getItem('msal.idtoken')}`
      // request.headers.common['Authorization'] = `Bearer ${res.accessToken}` // This doesn't work
    }

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 33 (16 by maintainers)

Most upvoted comments

Guys,

This is a severe issue with a Microsoft product. Can we please get an update and an ETA to fix?

Cheers

Updated workaround: getting msal.idtoken from the localStorage does not work reliably, as the value is sometimes (often) not correctly updated by msal.

This function seems to get the correct, up-to-date token more reliably:

function extractMSALToken() {
  const timestamp = Math.floor((new Date()).getTime() / 1000)
  let token = null

  for (const key of Object.keys(localStorage)) {
    if (key.includes('"authority":')) {
      const val: any = JSON.parse(localStorage.getItem(key)!)

      if (val.expiresIn) {
        // We have a (possibly expired) token

        if (val.expiresIn > timestamp && val.idToken === val.accessToken) {
          // Found the correct token
          token = val.idToken
        }
        else {
          // Clear old data
          localStorage.removeItem(key)
        }
      }
    }
  }
  if (token) return token
  throw new Error('No valid token found')
}

I’m experiencing the same issue, which is causing my app to stop working every 24 hours (when token expires).

As a workaround I have to manage token expiration and clearing localstorage with dedicated code, which is far from ideal (and duplicates what MSAL should be doing) - the workaround could stop working anytime MSAL decides to use different storage keys, ie. I would prefer not knowing how the lib use localstorage and not deal with internal implementation details.

Could you provide an ETA for the fix?

@scambier Ah, that would explain why you are getting back an ID token. To properly protect a custom web API using AAD access tokens, you need to register a custom scope in the Azure Portal, and then pass that custom scope to your login call as well as acquireTokenSilent. Your web API will need to use one of our middlewares to verify the access token.

See this comment for more info: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/1040#issuecomment-540810355

@sameerag actually, it seems to still not work as expected, but maybe I’m misunderstanding. My setup is still the one described in the first post, but MSAL still often fails to refresh the token by itself.

Basicall, each ajax request I do is preceded by a call to this function:

  public async getAuthResponse(): Promise<Msal.AuthResponse> {
    try {
      return await this.userAgentApp.acquireTokenSilent(this.tokenRequest)
    }
    catch (e) {
      try {
        return await this.userAgentApp.acquireTokenPopup(this.tokenRequest)
      }
      catch (e) {
        console.error('Cannot retrieve token')
        throw e
      }
    }
  }

So, 401 responses shouldn’t happen, right? Because they still happen. I have to intercept those responses, call acquireTokenSilent/Popup() once more, inject that new token, and replay the request.

Edit: it’s almost like the acquireSilent() is sending a false positive “your token is still fresh”, when actually it should have caught an error to subsequently call acquirePopup() or acquireRedirect()

Edit 2: the minimal code for my axios response interceptor:

    axios.interceptors.response.use(
      response => response,
      async (error) => {
        if (error.config && error.response && error.response.status === 401) {
          const res = await authService.getAuthResponse() 
          error.config.headers['Authorization'] = `Bearer ${res.accessToken}`
          return axios.request(error.config)
        }
      })

@sameerag I’ve updated a few hours ago, and so far the token seems to refresh itself correctly, thanks.

@mxswat Yes, if you are using access tokens for a custom API (e.g. an Azure Function), you should create a custom scope.

@jasonnutter I was wrong on my assumptions: the accessToken is not invalid, it’s null. So yeah, it’s normal to have a 401 response, since I’m sending “Bearer null” as my Authorization. But I guess it’s not normal to have a null accessToken

However, everything else in the acquireToken() response is valid, even the idToken.rawIdToken. So when I re-query acquireToken(), the accessToken is correctly filled, and is identical to the previous idToken.rawIdToken.

This was the response I got today at 09:48 GMT. The expiration value is good (valid until 10:41 GMT) image

Edit: what’s the role of idToken.rawIdToken?

Adding this bug to our October 2019 miletsone

Is there any specific reason why the id token is getting injected into your Authorization header and not the access token? The workaround seems succinct, apart from this one line.

Cheers!

@tylerjwatson I have two objects containing an access_token in the localStorage (cf. my second screenshot), and their keys (that are also objects) don’t make them easy to retrieve and compare, so I’m just picking the msal.idtoken value. Since it’s a workaround, I didn’t give it much thought and went with the first “it seems to work” solution