AzureAD / microsoft-authentication-library-for-js

Microsoft Authentication Library (MSAL) for JS
http://aka.ms/aadv2
MIT License
3.62k stars 2.64k forks source link

acquireTokenSilent doesn't renew the id-token #4206

Open janandreschweiger opened 2 years ago

janandreschweiger commented 2 years ago

Core Library

MSAL.js v2 (@azure/msal-browser)

Core Library Version

2.18.0

Wrapper Library

Not Applicable

Wrapper Library Version

None

Description

Hi Micrsoft communiy,

We have a react app, which implements your library as described below. We only use it to get an id-token for validating the Microsoft account on our server. Unfortunately, after an hour the id-tokens that are returned by the acquireTokenSilent function are expired.

We have tried to set tokenRenewalOffsetSeconds to 300, but this doesn't resolve the issue. If we logout, clear the cache or refresh the site a few times, we get a valid token again. But this is unfortunately, very unpleasant for our users.

We get a lot of customer complaints on a daily basis, because of this issue. Please help us. We are thankful for any suggestions. Thanks!!

Error Message

No response

Msal Logs

Here are some logs. Please ignore the "DEBUG" in front of the messages.:

DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:handleRedirectStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : [5c24a3aa-1095-42c9-85a2-d71a097390a4] : msal.js.browser@2.18.0 : Info - handleRedirectPromise called but there is no interaction in progress, returning null. log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:handleRedirectEnd log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:08:42 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess log.js:16:10

// we get an expired id-token

// we refresh the page

// we get the same logs as above and get an expired id-token

// we refresh the page again

DEBUG | Message: [Mon, 01 Nov 2021 16:09:53 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:handleRedirectStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:53 GMT] : [2e893bd0-832f-4730-8cae-22e5108dada7] : msal.js.browser@2.18.0 : Info - handleRedirectPromise called but there is no interaction in progress, returning null. log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:53 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:handleRedirectEnd log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:53 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenFromNetworkStart log.js:16:10
Some cookies are misusing the recommended “SameSite“ attribute 2
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenStart log.js:16:10
DEBUG | Message: [Mon, 01 Nov 2021 16:09:54 GMT] : @azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenSuccess

// we suddenly get a valid id-token and everything works again as expected

MSAL Configuration

{
    auth: {
      authority: 'https://login.microsoftonline.com/common',
      clientId: <client-id>,
      postLogoutRedirectUri: window.location.origin,
      redirectUri,
      validateAuthority: true,
      navigateToLoginRequestUrl: false,
    },

    system: {
      loggerOptions: {
        loggerCallback: (level, message, containsPii) => {
          console.log(message);
        },
        piiLoggingEnabled: false  // disables personal information
      },
      windowHashTimeout: 60000,
      iframeHashTimeout: 10000,
      loadFrameTimeout: 0,
      // I also tried: tokenRenewalOffsetSeconds: 300
    },

    cache: {
      cacheLocation: 'localStorage',
      storeAuthStateInCookie: true
    }
}

Relevant Code Snippets

const msalScopes = [ 'openid', 'User.Read' ];
const graphScopes = [ ];
const config = ... // see above
const msalClient = new PublicClientApplication(config);

// login (not essential):
const login = async () => {
  const accounts = msalClient.getAllAccounts();
  if (accounts || accounts.length < 1) {
    let tokenResponse = await msalClient.handleRedirectPromise();
    const accountObj = tokenResponse
      ? tokenResponse.account
      : msalClient.getAllAccounts()[0];

    if (!tokenResponse) {
      if (accountObj) {
        // User has logged in, but no tokens:
        try {
          tokenResponse = await msalClient.acquireTokenSilent({
            account: msalClient.getAllAccounts()[0],
            scopes: msalScopes,
          });
        } catch (err) {
          await msalClient.acquireTokenRedirect({ scopes: msalScopes });
        }
      } else {
        // No accountObject or tokenResponse present. User must now login:
        await msalClient.loginRedirect({ scopes: msalScopes });
      }
    }
  }
}

// get id-token (here is the problem):
async getIdToken(scopes=null) {
  const accounts = msalClient.getAllAccounts();

  const { idToken } = await msalClient.acquireTokenSilent({
    account: accounts[0],
    scopes: (scopes ? scopes : [...msalScopes, ...graphScopes]).filter(onlyUnique)
  });
  return idToken;
}

Reproduction Steps

  1. Implement the code above
  2. login
  3. get an id-token
  4. wait 1 hour
  5. get another id-token, which is expired

Expected Behavior

The id-token should be refreshed before it expires.

Identity Provider

Azure AD / MSA

Browsers Affected (Select all that apply)

Chrome, Firefox

Regression

No response

Source

External (Customer)

nils-tahler commented 2 years ago

I experience the exact same issue. Could you please have a look on this one @tnorling?

tnorling commented 2 years ago

What happens if you provide forceRefresh: true on the acquireTokenSilent request object?

janandreschweiger commented 2 years ago

Thanks for your answer @tnorling.

This would probably resolve the issue, as I get the following additional log:

@azure/msal-browser@2.18.0 : Info - Emitting event: msal:acquireTokenFromNetworkStart

This issues normally occurs after 1h, so we would have to wait. But please have a look at my logs posted above. The issue is resolved once the acquireTokenFromNetworkStart log indicates that a new token is fetched.

tnorling commented 2 years ago

I suspect this is happening when the access token has a longer validity than the id token, if that's the case we'll need to re-evaluate how we do cache lookups today. We currently only check the access token validity because we made a naive assumption that the cached idToken would always have at least as much time left as the access token.

If setting forceRefresh resolves this please use that as a workaround for now until we're able to put out a fix.

@hectormmg will follow up as the current on call engineer

janandreschweiger commented 2 years ago

Thank you so much @tnorling for your help! I will write you back, if this resolved the issue.

janandreschweiger commented 2 years ago

Yes, this option resolved my issue for now. Nevertheless, it would be nice to use cached tokens again. Thanks for your help @hectormmg!

hectormmg commented 2 years ago

@janandreschweiger thanks for updating us, I'll start looking into factoring in ID token expiration into the cache logic.

joshpitkin commented 2 years ago

I am seeing the same issue in my apps. I chose a variation on the workaround to dynamically set the forceRefresh option based on the id_token being expired. If you have LOTS of API calls using the id_token leveraging the cache is very important..

        const idTokenClaims: { exp?: number } = this.authService.instance.getActiveAccount().idTokenClaims;
        const forceRefresh = (new Date(idTokenClaims.exp + 1000) < new Date()); 

        this.authService.instance
            .acquireTokenSilent({
              forceRefresh: forceRefresh
            }).then((authResponse) => {
              // do something with authResponse.idToken 
            });

Looking forward to fix in MSAL library.

jasonnutter commented 2 years ago

@janandreschweiger @joshpitkin @nils-tahler Any specific reasons you are validating the id token, instead of an access token (e.g. using a custom scope)?

joshpitkin commented 2 years ago

That is a good question. In my situation we have multiple web apps using the library that are connecting with several different WebAPIs and passing in the id_token as the Auth credential. It has been that way for years, starting with ADAL.js and recently supporting MSAL also. All of our apps are internal so we are less concerned about setting up unique permissions / tokens per API as it just adds complexity and maintenance. We only need to validate the user's identity (username) and that the token came from a valid source. It is on the roadmap to migrate some things over to accessTokens, however we also have another complex internal system for controlling access to different resources and don't want to maintain it in duplicate with the accessToken mechanism..

janandreschweiger commented 2 years ago

Same here. We have a daemon for Microsoft Graph that retrieves and stores all the data of an organization in our database. Therefore, we only need to authenticate the user.

ghost commented 2 years ago

This issue requires attention from the MSAL.js team and has not seen activity in 5 days. @pkanher617 please follow up.

its-miller-time commented 2 years ago

I believe I am having a similar issue. We need to verify that users sending requests to our API's have permissions. Until recently, we have been using the ID token without issue (PKCE Flow). We tried switching to the access token, but the signature on the token cant be verified because it has a nonce and is specific for graph. Its been a very circular process in my googling....users who cant verify access token signatures say use idToken, but here it sounds like its not best practice to use idToken and accessToken is preferred. Neither works for the above reasons.

tnorling commented 2 years ago

@its-miller-time In your scenario (and in most scenarios) you should be using an access token. There are very few scenarios where we would recommend using an idToken for anything other than getting basic information about the user (e.g. email address, name, etc.). Anytime you need to validate the user has permission to access something you should use an access token.

The reason your access token is scoped to MS Graph is because you're either requesting MS Graph scopes (User.Read for instance) or you're not requesting scopes at all (MSAL.js will default to openid and profile which returns a Graph access token if no other, more specific scopes are requested). To acquire an access token for your own resource you should create a custom scope on your app registration and request that scope when acquiring a token with MSAL.

its-miller-time commented 2 years ago

@tnorling thanks for the reply. Yes we are requesting graph scopes. We were wanting to simply pass the headers along and validate the token in our API but it doesn't sound like that's possible? From what I gather the recommended/only solution would be to make an app registration specifically for our api's and request an additional token? Or will a custom scope allow us to use the same access token?

On a side note: the ID token had been working fine for a year prior to this. Did something change in the ID token lifetime to where it expires sooner now?

tnorling commented 2 years ago

@its-miller-time

We were wanting to simply pass the headers along and validate the token in our API but it doesn't sound like that's possible? From what I gather the recommended/only solution would be to make an app registration specifically for our api's and request an additional token? Or will a custom scope allow us to use the same access token?

Access tokens are scoped to a single resource. If you need to access MS Graph APIs in addition to your own API you need 2 access tokens. However, this does not mean you need more than 1 app registration, you can register as many API permissions as you need on a single app registration.

On a side note: the ID token had been working fine for a year prior to this. Did something change in the ID token lifetime to where it expires sooner now?

The issue here is that AAD changed access tokens lifetimes from a static 60 minutes to a variable 60-90 minutes and MSAL.js only checks access token lifetimes. Id tokens still have a static 60 minute lifetime.

m-sterspace commented 2 years ago

@its-miller-time In your scenario (and in most scenarios) you should be using an access token. There are very few scenarios where we would recommend using an idToken for anything other than getting basic information about the user (e.g. email address, name, etc.). Anytime you need to validate the user has permission to access something you should use an access token.

I've also run into this bug on my application, but I was just curious about why you always want to use access tokens over idTokens? If your application is implementing role based authentication, you can store the roles with your Azure App Registration and have them returned as claims, but you can also store that information with your user account in your database. It means that you might have to do an extra db fetch at the api layer to get the user's role, but it shouldn't be any less secure if I'm understanding things correctly. I've generally found that it simplifies your application if Msal is just handling identity verification, and your application manages all permissions and permission validation separately.

janandreschweiger commented 2 years ago

Hi @tnorling, any updates so far?

We currently use @joshpitkin's workaround. However, we had to change the '+' to a '*'.

tnorling commented 2 years ago

Unfortunately I have no updates and no ETA as this will need to be prioritized against our other efforts. Our recommendation is still to use access tokens whenever possible and if there's anything preventing you from using access tokens please do let us know and we would be happy to help you resolve whatever issues you are facing.

markusberg commented 2 years ago

I've been using the idToken because it lets me map AD-group memberships to application roles, and those claims are automatically available on both the client and the server. Is there a way for me to do that with access tokens?

tnorling commented 2 years ago

@markusberg Yes, the role and group claims are also available on access tokens. You just need to make sure the audience of the access token you request is that of your application. You can do this by registering a custom scope on your app registration.

long2know commented 2 years ago

I see the same issue with idTokens not being refreshed and I'm running @angular/msal-browser 2.21.

While I appreciate that access tokens are recommended, in my service, the backend validates the idToken w/ .NET Core middleware and then uses a custom Role based system. The design is like this as pre-2.x versions of MSAL worked without issue to retrieve idTokens. No extra scopes/consent were required and we could simply pass ClientId to get the idToken. Additional scopes/resource access, since we use a custom Role-based system, are not needed. Also, since I'm using "First Party" AAD (4-5 instances), it becomes painful to add custom scopes since there are encryption requirements when additional scopes are added. It's a fair number of hoops to jump through to make something work, tbh.

At any rate, it has become somewhat problematic in that the idToken will be expired, won't be refreshed, and then 401 errors are returned on the API calls. It seems the only work-around for the users has been to force a logout in the AAD tenant. I may try "forceRefresh" in conjunction with checking the expiry, but this does potentially cause quite a bit more traffic back to the AAD tenant. On a related note, it would be nice if it were possible to tell MsalInterceptor to use the idToken instead of accessToken attaching to auth headers.

HenryY2J commented 2 years ago

I am seeing the same issue in my apps. I chose a variation on the workaround to dynamically set the forceRefresh option based on the id_token being expired. If you have LOTS of API calls using the id_token leveraging the cache is very important..

        const idTokenClaims: { exp?: number } = this.authService.instance.getActiveAccount().idTokenClaims;
        const forceRefresh = (new Date(idTokenClaims.exp + 1000) < new Date()); 

        this.authService.instance
            .acquireTokenSilent({
              forceRefresh: forceRefresh
            }).then((authResponse) => {
              // do something with authResponse.idToken 
            });

Looking forward to fix in MSAL library.

Unfortunately, this approach doesn't work (at least for me): instance.getActiveAccount() always returns null, so i tried account from hook ( const msAccount = useAccount(accounts[0] || {}) where accounts is object array from useMsal() hook), but it always keeps the first acquired idToken and doesn't refresh data for account after acquireTokenSilent call with forceRefresh:true, so after first token is expired it always set forceRefresh to true.

here is my code snippet, maybe i'm doing something wrong:

//...
const defaultScopes= ["openid", "profile"];
//...
    const {instance, accounts} = useMsal();
    const msAccount = useAccount(accounts[0] || {});
    const buildRequestMethod = async ()=> {
        try {
            const request = async (apiCallConfig) => {

                const  { exp } = msAccount.idTokenClaims;
               //const activeAcc = instance.getActiveAccount() - always null
                const expDate= new Date(exp * 1000), now = new Date();
                const forceRefresh = (expDate<now);
                const tokenResp = await instance.acquireTokenSilent({scopes:defaultScopes, account: msAccount, forceRefresh: forceRefresh });
                const jwt = tokenResp.idToken;
                //...
sameerag commented 2 years ago

The initial assumption we made when we designed the library is the uniformity between expiry times for id_token and access_token. However, this is not true in all cases and we need to enhance the library to support these various expiry times. Added this to our internal backlog.

However, please note that this would need some redesign from our end and hence will take some time.

florianfischerx commented 2 years ago

Hi, we are facing this exact same issue. The workaround by forcing a refresh is suboptimal for various reasons :- \ Is there any eta for an update for this?

jmriyaz84 commented 1 year ago

Dear Team , I am also facing same issue , my idToken is not gettng renewed after expiry , it works when i set forceRefresh as true , but still some user face authentication issue , in Vue2 we use ADAL library , but migrating to vue3 adal got deprecated so we started using msal , but we are facing lot of issues while using msal . Please help.

fong1999 commented 1 year ago

Hi Everyone,

We have the same challenge. We use the id token to call a vendor API. But it expires around 65 minutes. What we do has a few steps.

  1. Check the existing id token exp attribute by decoding the id token.
  2. If it is going to expire in 5 minutes, we call MSAL again by setting forceRefresh flag to true to get a new id token
  3. Otherwise, we continue to use the cached id token.

I did notice that MSAL library check only access token's lifetime which expires around 86 minutes, having a longer life time than id token which has around 65 minutes. the 21 minutes in between is causing a 403 error when we are trying to call the vendor API.

As tnorling stated on 11/1/2021:

I suspect this is happening when the access token has a longer validity than the id token, if that's the case we'll need to re-evaluate how we do cache lookups today. We currently only check the access token validity because we made a naive assumption that the cached idToken would always have at least as much time left as the access token.

If MSAL could check both id token and access token, or whichever has shorter lifetime, then get a new set of tokens, which will solve this issue. Hope this can be worked on in the near future.

Thanks!

sameerag commented 1 year ago

cc @EmLauber. We are aware of this issue and there is no easy solution here. I would like us to track this internally if not there already.

fong1999 commented 1 year ago

@sameerag @EmLauber Thank you very much!

peterzenz commented 1 year ago

I'm going to drop a link on ID Tokens as a reference if folks have additional questions on ID Token usage.
https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens

For this particular issue we're going to have to make an API/interface change to address this to request explicit token types. For instance, you should be able to just request an Access Token without a Refresh Token, or just an ID Token. We've got an internal item tracking for this work for a future major release. In the interim, this won't be changed for MSAL.js v2.x.y releases.

jihu commented 1 year ago

If you look into this, I suggest that you also consider the use case where there is no accessToken at all. Currently, when not using any custom scopes, so that the identity acccess managment (Azure B2C in our case) won't issue an accessToken, the acquireTokenSilently method doesn't handle that properly. It should handle cases where there is only an idToken available, and no accessToken.

michmerr commented 6 months ago

I'm running into this with

@azure/msal-browser@3.7.0 @azure/msal-common@14.6.0 @azure/msal-react@2.0.9

When I dump the tokens returned from acquireTokenSilent, I see that the ID token has a shorter lifespan than the access token:

Access Token:
Issued: Thu Mar 07 2024 10:28:46 GMT-0800 (Pacific Standard Time)
Expires: Thu Mar 07 2024 11:44:10 GMT-0800 (Pacific Standard Time)
ID Token:
Issued: Thu Mar 07 2024 10:28:46 GMT-0800 (Pacific Standard Time)
Expires: Thu Mar 07 2024 11:33:46 GMT-0800 (Pacific Standard Time)

This call was made at 11:33:37 GMT-0800, so it's also possible that the ID token expiry was checked, but in a simple way that did not recognize it as about to expire.

edit: Looked at the source, and it doesn't check the id token at all.

toms-gribusts commented 3 months ago

Having the same issue while using the latest msal packages. ID token in not refreshed when calling acquireTokenSilent() even if it has expired.

"@azure/msal-browser": "^3.17.0", "@azure/msal-react": "^2.0.19",

MaxCadmanBC commented 1 month ago

Version:"@azure/msal-browser": "^3.15.0"

Just adding to the above that I'm also running into the ID token refresh issue when configured for NAA using createNestablePublicClientApplication with the following config:

const msalConfig = {
    auth: {
        clientId: applicationId,
        authority: 'https://login.microsoftonline.com/common',
        supportsNestedAppAuth: true,
    },
};

However, the current workaround to use forceRefesh doesn't appear to resolve the issue, and having a quick look through the source code, I can't seem to see if NAA supports forceRefresh?

mickael9 commented 1 month ago

According to MS documentation

The default lifetime of an access token is variable. When issued, an access token's default lifetime is assigned a random value ranging between 60-90 minutes (75 minutes on average)

So if I want the ID token (which always expires after 1 hour) to always be renewed at most 5 minutes before its expiration, I can just set tokenRenewalOffsetSeconds to 35 minutes:

const msalConfig = {
  // ...
  system: {
    // acquireTokenSilent only looks at the expiration time of the access token to determine when it needs
    // to refresh the tokens (because it wrongly assumes all tokens have the same lifetime).
    // Microsoft changed their backend and the access token now have a random lifespan between 60 and 90 minutes
    // while the ID token has a fixed 1 hour lifetime (at least by default)
    // This means acquireTokenSilent can return expired ID tokens from cache for at most 30 minutes
    // To work around this, we set the renewal offset to 35 minutes before the accessToken expires
    // which will translate to 5 to 35 minutes before the ID token expires
    // Ref: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/4206
    tokenRenewalOffsetSeconds: 35 * 60,
  }
};