AzureAD / microsoft-authentication-library-for-js

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

token renewal operation failed due to timeout MSAL #1592

Closed ashishbhulani closed 4 years ago

ashishbhulani commented 4 years ago

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.

ghost commented 4 years ago

@ZacharyHiggins-dbMotion I'm not sure if understand what does it mean that you had redirectURI set to a page that activated MSALGuard .

In the dev you should just have it set to http://localhost:4200. In the prod it should be set to the prod address.

Any redirections after the authentication should be handled using Angular routing: { path: '', redirectTo: 'main', pathMatch: 'full', canActivate: [MsalGuard] },

In a simple scenario you don't even need to invoke any Msal methods manually, but just let MsalGuard to handle the authentication (in such case you also need to pass { prompt: 'select_account' } to MsalAngularConfiguration.extraQueryParameters config, if you want force login on every page load)

That's correct. To be clear, my issue was not with configuring a matching redirectURI in Azure, my appmodule.ts (MSALConfigFactory), or the intricacies of managing different addresses between a prod, local dev environment, and the others.

The redirectURI I'm referring to is the one configured in our MSALConfigFactory (and obviously matching in our Azure AD app config)

function MSALConfigFactory(): Configuration {
  return {
    auth: {
      clientId: environment.ClientId,
      authority: environment.AuthorityServer,
      validateAuthority: true,
      redirectUri: environment.RedirectURL,
      postLogoutRedirectUri: "",
      navigateToLoginRequestUrl: false,
    },
    cache: {
      cacheLocation: "sessionStorage",
      storeAuthStateInCookie: false, // set to true for IE 11
    },
    system: {
      loadFrameTimeout: 30000,
    }
  };
}

This was a separate issue though, and it appears to only have surfaced because of our previous implementation. Since the timeout error wasn't very intuitive output for our particular issue, we had to do some discovery and make some assumptions (and a lot of reading). The way achieve our original spec is pretty clear now given your advice (thank you again).

To reiterate, our original implementation was incorrect because we were trying to trigger the login, and handle the routing programmatically in the appcomponent.ts, None of this was needed and we just had to set the app router up correctly.

If you look at the demo project... there is a "login" button, and some code behind it. I was essentially running that in ngOnInit. Obviously not right, but was not immediately clear until I had a chance to sit down and look at it.

ghost commented 4 years ago

Clarification - We wanted the user to be automatically presented a login page, which is why we essentially added loginredirect() in ngOnInit in appcomponent. Regarding the passing the select_account param in the loginredirect method. I think this was part of our problem (it's been a while), and why we were doing this in ngOnInit... so we could pass these parameters. Our request actually looked like this:

  request = {
    scopes: ["user.read"],
    prompt: 'login'
  }

It's still not clear how with our current implementation, that we can force this if we aren't actually calling loginRedirect({ prompt: 'select_account' })) anywhere.... I will need to do more research. It's likely very simply, but that particular workflow is no longer a priority for this project, but this seems out of scope with the issue being tracked here. Our conversation definitely helped with clarifying the proper implementation.

anth-git commented 4 years ago

@ZacharyHiggins-dbMotion As for _selectaccount, add it to MSAL_CONFIG_ANGULAR provider factory:

{
    provide: MSAL_CONFIG_ANGULAR,
    useFactory: MSALAngularConfigFactory
}

function MSALAngularConfigFactory(): MsalAngularConfiguration {
  return {      
      extraQueryParameters: {
        prompt: 'select_account'
      }
    }
}
ghost commented 4 years ago

@ZacharyHiggins-dbMotion As for _selectaccount, add it to MSAL_CONFIG_ANGULAR provider factory:

{
    provide: MSAL_CONFIG_ANGULAR,
    useFactory: MSALAngularConfigFactory
}

function MSALAngularConfigFactory(): MsalAngularConfiguration {
  return {      
      extraQueryParameters: {
        prompt: 'select_account'
      }
    }
}

My friend, you have been very very helpful today. I haven't even started Googling that particular problem yet. Thank you very very much. I'll give that a shot in about an hour :)

ghost commented 4 years ago

Following up. Everything is running flawlessly now. Thank you very much! Have a great weekend! 👍

gunnamvasavi commented 4 years ago

Hi @ZacharyHiggins-dbMotion and @anth-git.... I'm also facing the same issue and the scenario is like users have to be logged in for all the page routes and using MSALGUARD for that. Could you explain me if any workaround is there to avoid "token renewal failed due to timeout". And my code is as follows:

app.module.ts:


 MsalModule.forRoot({
      auth: {
        clientId: environment.clientId,
        authority: "https://login.microsoftonline.com/"+environment.tenatId,
        validateAuthority: true,
        redirectUri: environment.REDIRECT_URI,
        postLogoutRedirectUri: environment.REDIRECT_URI,
        navigateToLoginRequestUrl: true,
      },
      cache: {
        cacheLocation: 'localStorage',
        storeAuthStateInCookie: isIE, // set to true for IE 11
      },
    },
      {
        popUp: false,
        consentScopes: [
          'user.read',
          'openid',
          'profile',
        ],
        unprotectedResources: [],
        protectedResourceMap: [
          ['https://graph.microsoft.com/v1.0/me', ['user.read']]
        ],
        extraQueryParameters: {}
      })
 providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true
    },
    { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true },
  ],

routing-module:

const routes: Routes = [
  // { path: '', redirectTo: '/EL', pathMatch: 'full' },
  {
    path: '',
    component: ExecHomeComponent,
    canActivate: [MsalGuard],
    children:[
      {
        path: ':main/:sub',
        component: OtherPageComponent,
        canActivate: [MsalGuard],
      },
      {
        path: ':main/:sub/:classification',
        component: OtherPageComponent,
        canActivate: [MsalGuard],
      },
      {
        path:'',
        component:LandingComponent,
        canActivate: [MsalGuard],
      },  
    ]
  }
];

based on the role i'm handling some redirects in app.component.ts ngOnInit


 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']]);
    }

Help me out! As we are launching this to a bunch of users in the next week, I wanted to fix this issue.

anth-git commented 4 years ago

@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

lukan95 commented 4 years ago

I try @anth-git approach. If i run it on localhost, it works fine. But if i try run it on a production server, only a blank page will be displayed.

Im using also hash strategy and msalguards.

Is there a need for a refresh or something else?

@anth-git I found a difference between the local (dev) version and the production version. In the dev version of the build is aot set to false. At the production I have aot turned on. If I set in production build aot to false, it works for me, , but otherwise routing probably doesn't work.

anth-git commented 4 years ago

@lukan95 It works for me with AOT just fine, besides this is a default setting since Angular 9. I don't know why it could make a difference in your case though.

Assuming that your issue is related to a timeout error, did you try to change POLLING_INTERVAL_MS to some low value, for example 5ms? (node_modules\msal\lib-es6\utils\WindowUtils.js). This way you could find out if something removes hash before library can parse it or is it something else.

lukan95 commented 4 years ago

@anth-git I am using Anguler 8 and there is default setting without aot. So I set it in tsconfig.json. Maybe I'll try to update angular to version 9.

Yes i tried and it was okay.

gunnamvasavi commented 4 years ago

@anth-git your approach worked for me. Tried in local and prod as well its working! Thanks for sharing work around.

lukan95 commented 4 years ago

I changed the angular version from 8 to 10 and the problem with active aot is no longer and the @anth-git approach works flawlessly. Thank you.

Rookian commented 4 years ago

@ashishbhulani You should be able to handle this without modifying the interceptor. When you make the http request and you receive an error, if it is a timeout or interaction required error, invoke acquireTokenPopup with the same set of scopes, and then make the request again.

const GRAPH_ENDPOINT = "https://graph.microsoft.com/v1.0/me";

this.http.get(GRAPH_ENDPOINT)
    .subscribe({
      next: (profile) => {
        this.profile = profile;
      },
      error: (err: AuthError) => {
          this.authService.acquireTokenPopup({
            scopes: this.authService.getScopesForEndpoint(GRAPH_ENDPOINT)
          })
          .then(() => {
            this.http.get(GRAPH_ENDPOINT)
              .toPromise()
              .then(profile => {
                this.profile = profile;
              });
          });
      }
    });

Note, if you want to use acquireTokenRedirect or loginRedirect instead, your application will need to implement handleRedirectCallback separately, not inside the interceptor or where you make the http request. Instead, it needs to be invoked on page load, as demonstrated in the Angular 8 sample. Remember, if you call redirect, the browser will fully redirect away from your application and lose all context, which is why we recommend acquireTokenPopup instead for this scenario.

The link does not work anymore. Is there a complete working example for angular anywhere?

jasonnutter commented 4 years ago

Updated link: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-angular-samples/angular8-sample-app/src/app/app.component.ts

vamshinarra commented 4 years ago

This issue is still present no change after using the iframe condition on router outlet

pawlikowski-michal commented 4 years ago

Does anyone was able to solve this issue using Angular 8? We tried almost every solution from this thread, but with no luck...

jwar-gilson commented 4 years ago

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

gunnamvasavi commented 4 years ago

On the routing @anth-git approach works fine, not seeing any issues but from the Interceptor side its not working. For Example I'm in a particular page and the token expires, clicking on some button in the UI would do an API call and at this point of time:

  Expected behavior: MsalInterceptor should take care of the token expiry and would give refreshed token.
  Actual behavior: Its throwing the same client token renewal failed error.

@jasonnutter and @anth-git are there any workarounds for this behavior?

Note: The latest version of MSAL@1.4.0 also not working for me. Its giving token renewal failed for each page routing and in Incognito mode it went on infinite loop.

joshpitkin commented 4 years ago

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.

tnorling commented 4 years ago

@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.

joshpitkin commented 4 years ago

thank you @tnorling this helps explain a lot. for now I will stick with 1.3.4 until we can use acquireTokenSilent to refresh just the idToken.

aruscus commented 4 years ago

@anth-git I'm not able to tag you in my issue directly. So, commenting here. Can you please look into this issue? I configured exactly how you mentioned. I still get some errors.

veera-raghavagupta commented 4 years ago

Hi @ZacharyHiggins-dbMotion and @anth-git.... I'm also facing the same issue and the scenario is like users have to be logged in for all the page routes and using MSALGUARD for that. Could you explain me if any workaround is there to avoid "token renewal failed due to timeout". And my code is as follows:

app.module.ts:


 MsalModule.forRoot({
      auth: {
        clientId: environment.clientId,
        authority: "https://login.microsoftonline.com/"+environment.tenatId,
        validateAuthority: true,
        redirectUri: environment.REDIRECT_URI,
        postLogoutRedirectUri: environment.REDIRECT_URI,
        navigateToLoginRequestUrl: true,
      },
      cache: {
        cacheLocation: 'localStorage',
        storeAuthStateInCookie: isIE, // set to true for IE 11
      },
    },
      {
        popUp: false,
        consentScopes: [
          'user.read',
          'openid',
          'profile',
        ],
        unprotectedResources: [],
        protectedResourceMap: [
          ['https://graph.microsoft.com/v1.0/me', ['user.read']]
        ],
        extraQueryParameters: {}
      })
 providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: TokenInterceptor,
      multi: true
    },
    { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true },
  ],

routing-module:

const routes: Routes = [
  // { path: '', redirectTo: '/EL', pathMatch: 'full' },
  {
    path: '',
    component: ExecHomeComponent,
    canActivate: [MsalGuard],
    children:[
      {
        path: ':main/:sub',
        component: OtherPageComponent,
        canActivate: [MsalGuard],
      },
      {
        path: ':main/:sub/:classification',
        component: OtherPageComponent,
        canActivate: [MsalGuard],
      },
      {
        path:'',
        component:LandingComponent,
        canActivate: [MsalGuard],
      },  
    ]
  }
];

based on the role i'm handling some redirects in app.component.ts ngOnInit


 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']]);
    }

Help me out! As we are launching this to a bunch of users in the next week, I wanted to fix this issue.

Can you please help if you have fixed the issue as I am facing same issue. I am using msal 1.4 and @azure/msal-angular 1.1.0 and custom interceptor

github-actions[bot] commented 4 years ago

This issue has not seen activity in 14 days. It will be closed in 7 days if it remains stale.

vishmay-AF commented 4 years ago

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?

jmckennon commented 4 years ago

@vishmay-AF This should have been fixed in msal@1.4.1, can you upgrade to the new version and if your problem still persists, open a new issue with your version, config, and usage?

vishmay-AF commented 4 years ago

@jmckennon Just now, my issues was solved by setting up azure configuration, I'm not sure which one did the trick, so I'm listing all...

  1. I gave some permission, (attached)
  2. created client secret,
  3. Exposed API with scope for impersonate user. I'ven't checked with 1.4.1, 1.4.0 is working for me. permissions .txt
rpapeters commented 4 years ago

Also getting token timeout error now and then. v1.4.0 was worse and v1.4.1 is better but also has other issues which should be resolved in 1.4.2 but not yet released. Could the origin of this problem be in the fact that both the MsalGuard and MsalInterceptor are trying to get a token very shortly after each other? (one for canActivate and one for a back-end request)

vishmay-AF commented 4 years ago

The occrance has been reduced since 1.4.1 but I'm still getting errors in some cases. it happens on the rediredction page after login. not all the time, it occurs for few times. mostly 1 out of 3 tries fails due to this and it affects my other working code too. this time MSAL gaurd does not cause trouble i think interceptor is the reason.

let me know if you have any solutions

Atleast if you have solution to catch the errors somewhere so it doesn't affect my code, then also its fine

rpapeters commented 4 years ago

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).

derekparsons718 commented 4 years ago

I hoped I wouldn't have to, but I got so tired of this bug that I ended up just upgrading to v2 (msal-browser) about a week ago, and I haven't seen the issue since.

aruscus commented 4 years ago

@derekparsons718 : Did you write a custom interceptor to add tokens to HTTP requests?

mattmesser commented 4 years ago

@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.

jasonnutter commented 4 years ago

@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.

We are looking to merge #2492 soon (hopefully today).

Have you done any debugging into what is causing the timeouts (they can happen for many reasons)? A common problem with Angular applications is that the Angular router clears the hash before MSAL has a chance to parse it. Setting initialNavigation to false or disabled when your app is inside an iframe has shown to help mitigate that specific root cause. Example: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/9fb15d5cc5ed4203f21ac067ea847d4592fd895c/samples/msal-angular-v2-samples/angular10-browser-sample/src/app/app-routing.module.ts#L32

jasonnutter commented 4 years ago

If that does not help, feel free to share MSAL logs and fiddler traces with me via email (which is in my profile).

daviddejaeger commented 4 years ago

I'm also still getting this error. Refreshing one or several times solves the problem but that is not really an option. I'm not doing anything special and following the demo samples. One difference is that the login code is inside a base.component because I have a resolver associated with that component with an api call to my backend for initialization logic instead of app.component.

app.component.ts: ngOnInit() {}}

app.component.html: <router-outlet></router-outlet>

app.module:

const routes: Routes = [
  { path: '', 
    component: BaseComponent, 
    canActivate: [MsalGuard],
    children: [ 
      { path: '', component: DeviationlistComponent, canActivate: [MsalGuard], },
      { path: 'unauthorized', component: UnauthorizedComponent, canActivate: [MsalGuard], },
      { path: 'myAssignments', component: MyassignmentsComponent, canActivate: [MsalGuard], },
      { path: 'deviations', component: DeviationlistComponent, canActivate: [MsalGuard], },
      { path: 'deviations/:id', component: DeviationdetailsComponent, canActivate: [MsalGuard], },
      { path: 'success', component: SuccessComponent, canActivate: [MsalGuard], },
      { path: 'CreateDeviation', component: DeviationcreateComponent, canActivate: [MsalGuard], },
      { path: 'deviationsfollowup', component: DeviationsFollowupComponent, canActivate: [MsalGuard,RoleGuardService,], data: { expectedRoles: ['Admin','QualityMember','QualityManager'] }},
      { path: 'charts', component: ChartsComponent, canActivate: [MsalGuard,RoleGuardService,], data: { expectedRoles: ['Admin','QualityMember','QualityManager'] }},
      { path: 'settings', component: SettinglistComponent, canActivate: [MsalGuard,RoleGuardService,], data: { expectedRoles: ['Admin']}}
    ],
    resolve: {
      data: AuthenticatedAppResolver
    }
  },
  {
    path:'performance',
    component: UnauthorizedComponent
  }
];

base.component:

ngOnInit() {
    let loginSuccessSubscription: Subscription;
    let loginFailureSubscription: Subscription;
    this.isIframe = window !== window.parent && !window.opener;
    this.checkAccount();

    loginSuccessSubscription = this.broadcastService.subscribe('msal:loginSuccess', () => {
      this.checkAccount();     
    });

    loginFailureSubscription = this.broadcastService.subscribe('msal:loginFailure', (error) => {
      console.log('Login Fails:', error);
    });

    this.broadcastService.subscribe('msal:logOut', () => {
      this.userService.removeUser();
    });

    this.subscriptions.push(loginSuccessSubscription);
    this.subscriptions.push(loginFailureSubscription);

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

    this.authService.setLogger(new Logger((logLevel, message, piiEnabled) => {
      console.log('MSAL Logging: ', message);
    }, {
      correlationId: CryptoUtils.createNewGuid(),
      piiLoggingEnabled: false
    }));
 }

  ngOnDestroy(): void {
    this.subscriptions.forEach((subscription) => subscription.unsubscribe());
  }

  checkAccount() {
    this.loggedIn = !!this.authService.getAccount();
    if (this.loggedIn){    
      let user = this.userService.getCurrentUser();
      this.username = user.givenName + ' ' + user.surName;
    }
  }

  login() {
    const isIE = window.navigator.userAgent.indexOf('MSIE ') > -1 || window.navigator.userAgent.indexOf('Trident/') > -1;
    if (isIE) {
      this.authService.loginRedirect();
    } else {
      this.authService.loginPopup();
    }
  }

  logout() {
    this.authService.logout();
  }

base.component.html:

<div class="sidebar">
    <app-navmenu></app-navmenu>
  </div>

  <div class="main">
    <div class="top-row px-4 mb-4">
      <app-companyselector></app-companyselector>
      Hello, {{username}}
      <a [routerLink]="" *ngIf="!loggedIn" (click)="login()">Login</a>
      <a [routerLink]="" *ngIf="loggedIn" (click)="logout()">Logout</a>
    </div>   
    <div class="content px-4">
      <!--This is to avoid reload during acquireTokenSilent() because of hidden iframe -->
      <router-outlet *ngIf="!isIframe"></router-outlet>
    </div>
  </div>

role-guard.service.ts:

export class RoleGuardService implements CanActivate {

  constructor(private authService: MsalService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot): boolean {
    const expectedRoles = route.data.expectedRoles;
    let authorized = false;

    expectedRoles.forEach(element => {
      if (this.authService.getAccount().idTokenClaims.roles.includes(element)) {      
        authorized = true;
      }
    });

    if (authorized){
      return true;
    }
    else{
      if (!this.isInIframe()){
        return false;
      }
      else{
        this.router.navigate(['unauthorized']);
        return false;
      }     
    }   
  }

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

authenticatedAppResolver.ts:

@Injectable()
export class AuthenticatedAppResolver implements Resolve<Observable<User>> {

    public constructor(private userService: UserService) { }

    public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User> {
        return this.userService.initializeUser();
    }
}

I wrapped all my API calls with the suggestion 'When you make the http request and you receive an error, if it is a timeout or interaction required error, invoke acquireTokenPopup with the same set of scopes, and then make the request again.' For example:

getCategories(): void {
    this.categoryService.getCategories().subscribe({next: (categories) => {
      this.categories = categories 
      this.changesMade = false;    
    },
    error: (err: AuthError) => {
      this.authService.acquireTokenPopup({
        scopes: this.authService.getScopesForEndpoint(environment.resources.deviationApi.resourceUri)
      })
      .then(() => {
        this.categoryService.getCategories()
          .toPromise()
          .then(categories => {
            this.categories = categories
            this.changesMade = false;
          });
      });
    }
  });
  }

However for the data-resolver I can't manage to do that because it requires an Observable. I'm a bit lost on how to proceed. Am I making an obvious mistake somewhere? Any help is appreciated.

mattmesser commented 4 years ago

@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.

We are looking to merge #2492 soon (hopefully today).

Have you done any debugging into what is causing the timeouts (they can happen for many reasons)? A common problem with Angular applications is that the Angular router clears the hash before MSAL has a chance to parse it. Setting initialNavigation to false or disabled when your app is inside an iframe has shown to help mitigate that specific root cause. Example:

https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/9fb15d5cc5ed4203f21ac067ea847d4592fd895c/samples/msal-angular-v2-samples/angular10-browser-sample/src/app/app-routing.module.ts#L32

@jasonnutter Thanks for the 'initialNavigation' suggestion. I'm hesitant to say it's definitively fixed yet: it usually occurs only after token expiration, which takes 24hrs. But things have improved significantly.

Just a note: In the config I also changed cacheLocation to 'sessionStorage' from 'localStorage'. I think this helped ameliorate several other issues that were cropping up.

To answer your question, yes I have spent quite a lot of time cumulatively debugging this. To me, it's been exceptionally difficult to precisely determine root cause. I've been unable to consistently replicate the error despite my best efforts. The timeout error bricks my dev-tools so my tried and true "debugger;" spamming did not work.

I'll reach out if things catch on fire again, or post here to confirm that the fix worked by Friday.

Thanks again!

jasonnutter commented 4 years ago

Summarizing recommended mitigations for v1:

The best mitigation is to upgrade to MSAL Angular v2, as we now have the first alpha version available: https://github.com/AzureAD/microsoft-authentication-library-for-js/releases/tag/msal-angular-v2.0.0-alpha.0

We have not heard about any timeout problems with v2, and strongly recommend all customers begin to upgrade.

I'm going to close this issue. If your Angular app continues to experience timeouts after the above mitigations, please open a new issue, thanks!

skironDotNet commented 4 years ago

@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);
       });
    }

This helped, the 3rd is where callback forces the redirect. Easy to test in chrome using dev console. Clear cache, and click some link in the app, the MsalGuard would not redirect without given solution

image