Angular MSAL (v2) login via redirect, redirecting 3 or more times before token acquisition

Viewed 1342

I have a Angular app (v11.2.0) that uses MSAL for authentication. I recently upgraded to v2 of the library (@azure/msal-angular - ^2.1.1, @azure/msal-browser - ^2.22.0), and it took some refactoring due to MSAL changes. It's mostly working as before with the exception of when it's authenticating a user and acquiring an authentication response: the process loops roughly 3 times before successfully getting an auth response, visibly refreshing the app each time. The approximate flow I'm seeing is as follows:

  • Loop 1
  • Navigate to app
  • User not authenticated
  • Handle redirect start
  • Handle redirect promise called but there is no interaction in progress, returning null
  • Handle redirect end
  • Login start
  • null authentication result received
  • Loop 2
  • Navigate to app
  • User not authenticated
  • Handle redirect start
  • Loop 3
  • Navigate to app
  • User not authenticated
  • Handle redirect start
  • Info - in acquire token call
  • Login success
  • Handle redirect end
  • Acquire token start
  • Authentication result received

Authentication is initiated and handled in my app.component.ts:

ngOnInit() {
   this.msalBroadcastService.inProgress$
      .pipe(
         filter((status: InteractionStatus) => status === InteractionStatus.None)
      )
      .subscribe(async () => {
         if (!this.authenticated) {
            await this.logIn();
         }
      })

   this.msalService.handleRedirectObservable().subscribe({
      next: (result: AuthenticationResult) => {
         if (!this.msalService.instance.getActiveAccount() &&             
            this.msalService.instance.getAllAccounts().length > 0) {
               this.msalService.instance.setActiveAccount(result.account);
            }
         },
      error: (error) => console.log(error)
   });
}

async logIn() {
   await this.msalService.instance.loginRedirect({
      scopes: ['user.read', 'openid', 'profile'],
      redirectUri: AppConfig.settings.authenticationconfig.redirectUri
   });
};

get authenticated(): boolean {
   return this.msalService.instance.getActiveAccount() ? true : false;
}

Has anyone experienced a similar situation or have any understanding of what's causing this behaviour and how to resolve it?

2 Answers

MSAL is quite infamous for this type of issues. Hardly anyone gets it right the first time. I can't exactly say what's wrong with your code, but I'll share what I use. Maybe it'll give you some pointers.

Instead of msalService.handleRedirectObservable(), I listen on msalBroadcastService.msalSubject$, like so;

this.msalBroadcastService.msalSubject$
  .pipe(filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS || msg.eventType === EventType.SSO_SILENT_SUCCESS))
  .subscribe((result: EventMessage) => {
    const payload = result.payload as AuthenticationResult;
    this.msalService.instance.setActiveAccount(payload.account);
  });

For me, the final solution came in three parts.

The first piece of the puzzle was following @Ε Г И І И О's answer, making the following change - Before:

// in app.component.ts        
this.msalService.handleRedirectObservable().subscribe({ 
   next: (result: AuthenticationResult) => {
      if (!this.msalService.instance.getActiveAccount() && this.msalService.instance.getAllAccounts().length > 0) {
         this.msalService.instance.setActiveAccount(result.account);
      }
   },
   error: (error) => console.log(error)
});

After:

// in app.component.ts       
this.msalBroadcastService.msalSubject$
   .pipe(
      filter(
         (msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS 
         || msg.eventType === EventType.SSO_SILENT_SUCCESS)
       ).subscribe((result: EventMessage) => {
          const payload = result.payload as AuthenticationResult;
          this.msalService.instance.setActiveAccount(payload.account);
    });

This stopped the looping behaviour but left the app in a hung state, with an interaction_in_progress BrowserAuthError. After some further digging, piece two was as follows - Before:

// in app.module.ts bootstrap: [AppComponent],

&

// in index.html
<body>
   <app-root></app-root>
</body>

After:

// in app.module.ts
bootstrap: [AppComponent, MsalRedirectComponent],

&

// in index.html
<body>
     <app-root></app-root>
     <app-redirect></app-redirect>
</body>

I had tried this previously with little success, but presumably, there's a tie-in to the first code change that works in conjunction with the second code change. The one final change I made was this - Before:

// in app.component.ts 
this.msalBroadcastService.inProgress$
   .pipe(
      filter((status: InteractionStatus) => status === InteractionStatus.None)
   )
   .subscribe(async () => {
     if (!this.authenticated) {
       await this.logIn();
     }
   });

After:

// in app.component.ts
this.msalBroadcastService.inProgress$
   .pipe(filter((status: InteractionStatus) => status === InteractionStatus.None)
   ,takeUntil(this._destroying$))
   .subscribe(async () => {
     if (!this.authenticated) {
       await this.logIn();
     }
   });

The app now behaves as desired, redirecting from the Microsoft login page and loading the app immediately - no more looping reloads!

Related