microsoft-authentication-library-for-js: token renewal operation failed due to timeout MSAL

Hi,

Library : “@azure/msal-angular”: “^1.0.0-beta.5” “msal”: “^1.2.2”

I have integrated MSAL library with my angular 8 application. Everything works fine except i keep getting an error token renewal operation failed due to timeout as soon as the token is expired. I was wondering how to fix this.

Below is the MSAL_config that i am using :

"auth": {
        
        "clientId": "xxxxx",
        "authority": "https://login.microsoftonline.com/xxx",
        "validateAuthority":"true",
        "postLogoutRedirectUri": "http://localhost:4200/",
        "navigateToLoginRequestUrl": true,
        "redirectUri":"http://localhost:4200/"
    },
    
    "scopes":["user.read", "openid", "profile"],
    "popUp": false,
    "unprotectedResources": ["https://www.microsoft.com/en-us/"],
    "protectedResourceMap":[["https://graph.microsoft.com/v1.0/me", ["user.read"]]],
    "system":{
        "loadFrameTimeout":10000
    },
    "cache": {
      "cacheLocation": "localStorage",
      "storeAuthStateInCookie":true

    }

I have created a new token interceptor which pull the token everytime a http request is amde. Below is the code, here i face issue when the token is expired and i get the error. Please help me to resolve this as it is affecting the project.

if (this.authService.getAccount()) {
 

                let token: string;
                return from(
                    this.authService.acquireTokenSilent(this.loginRequest)
                        .then((response: AuthResponse) => {
            
                            token = response.idToken.rawIdToken;
                            const authHeader = `Bearer ${token}`;
                            if (!request.headers.has('Cache-Control')) {
                                request = request.clone({ headers: request.headers.set('Cache-Control', 'no-cache' + '') });
                            }

                            if (!request.headers.has('Pragma')) {
                                request = request.clone({ headers: request.headers.set('Pragma', 'no-cache' + '') });
                            }

                            if (!request.headers.has('Expires')) {
                                request = request.clone({ headers: request.headers.set('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT' + '') });
                            }
                            return request.clone({
                                setHeaders: {
                                    Authorization: authHeader,
                                }
                            });

                        })
                )
                    .pipe(
                        mergeMap(nextReq => next.handle(nextReq)),
                        tap(
                            event => { },
                            err => {
                                if (err) {
                                    var iframes = document.querySelectorAll('iframe');
                                    for (var i = 0; i < iframes.length; i++) {
                                        iframes[i].parentNode.removeChild(iframes[i]);
                                    }
                                    debugger
                                    
                                    this.authService.handleRedirectCallback((err: AuthError, response) => {
                                        debugger
                                        this.authService.loginRedirect();
                                        if (err) {

                                            console.error('Redirect Error: ', err.errorMessage);
                                            return;
                                        }
                                        debugger
                                        this.authService.loginRedirect();
                                        console.log('Redirect Success: ', response);
                                    });
                                    this.broadcastService.broadcast('msal:notAuthorized', err.message);
                                }
                            }
                        )
                    );
            }

So in the above code it goes to the error but it never enters this.authService.handleRedirectCallback can someone please help me with this. I need the token to be renewed.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 9
  • Comments: 89 (16 by maintainers)

Most upvoted comments

I think I managed to get it working even for high POLLING_INTERVAL_MS values:

@NgModule({
  imports: [RouterModule.forRoot(routes,
    {
      enableTracing: false,
      useHash: true,
      initialNavigation: isInIframe() ? 'disabled' : undefined // <-THIS
    })],
  exports: [RouterModule]
})
export class AppRoutingModule {
}

export function isInIframe() {
  return window !== window.parent && !window.opener;
}

I’ve been able to resolve the token timeout issue by following the common issues guide found here: Common Issues

In our setup we had a redirect in the angular router from ‘/’ to ‘/some-url’ and we where redirecting msal to ‘/’, which would trigger a redirect by angular while acquiring the token. Msal did not like that. Now we have a redirect to (in our case) ‘/msal’ for msal, without angular redirecting under the hood and the problem seems to be resolved. Don’t forget to add the ‘/msal’ as an authenticated Redirect URI in the app registration in Azure Ad (B2C).

My question here is why does MSAL guard need to renew the token on each page request? even if the old token is not yet expired?

Yes, preventing navigation when you are in an iframe is a way to mitigate the behavior from the router that removes the hash. We show a similar workaround in the samples, but we’ll make sure this is better documented.

@jasonnutter I’m getting the same error. msal 1.3.1., msal-angular 1.0.0.

Auth only works right after I clear the browser cache. Hours later:

ERROR Error: Uncaught (in promise): ClientAuthError: URL navigated to is https://login.microsoftonline.com/<TenantId>/oauth2/v2.0/authorize?response_type=id_token&scope=openid%20profile&client_id=<ClientId>&redirect_uri=<RedirectUri>&state=<State>&nonce=<Nonce>&client_info=1&x-client-SKU=MSAL.JS&x-client-Ver=1.3.1&login_hint=<MyUsername>&client-request-id=<RequestId>&prompt=none&response_mode=fragment, Token renewal operation failed due to timeout.

Here’s my code. There are 3 places where I’m configuring/calling MSAL. I don’t think I’m doing anything fancy. I’m basing it on the Angular 8 sample app.

  1. app.module.ts -> NgModule -> imports:

    MsalModule.forRoot({
            auth: {
                clientId: "<ClientId>",
                authority: "https://login.microsoftonline.com/<TenantId>/",
                validateAuthority: true,
                redirectUri: '<SiteRoot>',
                postLogoutRedirectUri: "<SomeUrl>",
                navigateToLoginRequestUrl: true,        
            },
            cache: {
                cacheLocation : "localStorage",
                storeAuthStateInCookie: isIE
            },
            framework: {
                unprotectedResources: ['https://www.microsoft.com/en-us/'],
            }
        },
        {
            popUp: !isIE,
            consentScopes: [ "https://graph.microsoft.com/User.Read", "api://<ApiId>/API" ],
            extraQueryParameters: {},
            protectedResourceMap: [
                ['<SiteRoot>', ['api://<ApiId>/API']],
                ['https://graph.microsoft.com', ['user.read', 'openid', 'profile', 'email', 'offline_access']]
            ]
        })
    
  2. app.module.ts -> NgModule -> providers

    { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true }
    
  3. app.component.ts -> AppComponent -> ngOnInit

    ngOnInit() {
        this.authService.handleRedirectCallback((authError, response) => {
            if (authError) {
                console.error('Redirect Error: ', authError.errorMessage);
                return;
            }
    
            console.log('Redirect Success: ', response);
        });
    }
    

@jasonnutter @tnorling think this issue/library is going to be fixed before the end of the year? It’s absolutely bricking our spa. Our team is starting to catch a lot of heat for it since we put it in front of testers and they cant log in over 60% of the time. That, and issue #2492 have made testing our SPA impossible. It’s a nightmare.

@joshpitkin loginRedirect only requests an idToken, not an accessToken. The cache entry you are seeing that has scopes missing is actually the idToken. So when you call acquireTokenSilent it’s looking for accessTokens which, by design, do not exist yet. We will be addressing this in #2206, which will update acquireTokenSilent to look for and return an idToken if that is what was requested.

If your use case requires an accessToken you should call an interactive acquireToken method first, i.e. acquireTokenPopup or acquireTokenRedirect. If you are using msal@1.4.0 and you require only idTokens then this is a bug that will be addressed in the above PR. The workaround for now would be to downgrade to 1.3.4 in addition to calling the interactive methods first.

Ok, I spent a day on this so I will share some findings and in case it helps anyone else…

In my implementation I don’t need/want any special consent scopes yet, just openid and profile which I believe are default.

However the loginRedirect() -> saveAccessToken() method doesn’t ever save those values in the “scope” property of the JSON object key for the tokens, because the value is undefined and it the key is JSON.stringified.

Then whenever a call to the acquireTokenSilent() method is called it doesn’t find any cached tokens! Tracked this down to getCachedToken() -> getAllAccessTokens() which has a condition looking for the “scopes” property to exist in the key from the storage items. Later it also looks for the value to match the requested scope.

The result is that every time I call acquireTokenSilent() it queues up another login redirect request in the hidden iframe, and eventually too many back-to-back repeat calls were causing intermittent “timeouts” from MSAL in my case.

So is this intentional / by design or do I have something setup wrong? Additional consent scopes are not required are they?

As a workaround I am duplicating that storage item and adding a “scopes” property to it with the values of “openid” and “profile”, but I have to re-create that every time the tokens are not loaded from cache.

☝️ same. I get the error once in awhile, not all the time. Not good UX.

@gunnamvasavi Don’t navigate when in an iframe:

if (isInIframe()){
   return;
}

 if (!role) {
      let res = await this.authentication.getRole();
      role=localStorage.getItem('role');
      if(role){
        this.roleReady = true;
        this.router.navigate([this.globalListen.navList[role]['link']]);
      }
    } else {
      this.roleReady = true;
      if(this.path=='/')
      this.router.navigate([this.globalListen.navList[role]['link']]);
    }

function isInIframe() {
  return window !== window.parent && !window.opener;
}

Alternatively you can implement this https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-js-avoid-page-reloads

Apart from that, it may be necessary to disable initialNavigation: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/1592#issuecomment-664818059