Angular MSAL Library Concurrency Issues: msal-browser v3.2.0, Angular v16.2

451 views Asked by At

I am a junior developer creating a angular library for use across all of my orginizations future angular applications. My issue is I am getting "uninitialized_public_client_application" when my handleRedirectPromise fires. I have been working on this issue for longer than I care to admit. I have seen some similar post but they haven't seemed to work and it being a library has added a bit of extra complexity.

This is my test applications app.module.ts:

import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NgxSenateModule, MSAL_CONFIG_TOKEN, MsalAuthService } from 'ngx-senate';
import { HeaderComponent } from './header/header.component';
import { HomeComponent } from './home/home.component';
import { SecondBestLayoutComponent } from './second-best-layout/second-best-layout.component';
import { environment } from 'src/environments/environment';

export function msalInitializer(msalAuthService: MsalAuthService): Function {
  return () => msalAuthService.initialize();
}

@NgModule({
  declarations: [
    AppComponent,
    HeaderComponent,
    HomeComponent,
    SecondBestLayoutComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    NgxSenateModule
  ],
  providers: [
    {
      provide: MSAL_CONFIG_TOKEN,
      useValue: environment.msalConfig
    },
    { 
      provide: APP_INITIALIZER, 
      useFactory: msalInitializer, 
      deps: [MsalAuthService], 
      multi: true 
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

This is the relevant part of my msal-auth.service.ts in my library, an error is triggered when calling handleRedirectPromise. The emitters here are for my auth-guard to make sure it does not try to start until the initialization of the msal service has completed. Please let me know if you need any farther information!

import { Injectable, Inject, EventEmitter } from '@angular/core';
import { MSAL_CONFIG_TOKEN, MsalConfig } from './msal-config.token';
import {
  PublicClientApplication,
  LogLevel,
  AuthenticationResult,
  InteractionRequiredAuthError,
  Configuration,
} from '@azure/msal-browser';

@Injectable({
  providedIn: 'root'
})
export class MsalAuthService {
  private msalInstance?: PublicClientApplication;
  initialized = new EventEmitter<boolean>();

  constructor(@Inject(MSAL_CONFIG_TOKEN) private msalConfig: MsalConfig) {
  }

  async initialize(): Promise<void> {
    console.log("Initializing MSAL...");

    const msalConfig: Configuration= {
      auth: {
        clientId: this.msalConfig.clientId,
        authority: this.msalConfig.authority,
        redirectUri: this.msalConfig.redirectUri || window.location.origin,
        postLogoutRedirectUri: this.msalConfig.postLogoutRedirectUri || window.location.origin,
      },
      cache: {
        cacheLocation: "localStorage",
        storeAuthStateInCookie: false,
      },
      system: {
        loggerOptions: {
          loggerCallback: (level, message, containsPii) => {
            console.log(message);
          },
          piiLoggingEnabled: false,
          logLevel: LogLevel.Verbose,
        },
      },
    };

    const msalInstance = new PublicClientApplication(msalConfig);

    msalInstance
      .handleRedirectPromise()
      .then((tokenResponse) => {
        console.log("Msal Authenticated!");
        this.initialized.emit(true);
      })
      .catch((error) => {
        console.log("MSal authentication failed.");
        console.log(error);
        this.initialized.emit(false);
      })
  }

Edit: It seems that I needed to await the instance like this :

const msalInstance = new PublicClientApplication(msalConfigSettings);
    await msalInstance.initialize();

Unfortunately my guard now gets stuck awaiting for the msalservice to emit a value here:

export const canActivate: CanActivateFn = async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
  let config: AuthGuardConfig | null = null;
  try {
    config = inject(AUTH_GUARD_CONFIG_TOKEN);
  } catch {
    console.log("No provider for AUTH_GUARD_CONFIG_TOKEN.")
  }

  const msalService: MsalAuthService = inject(MsalAuthService);
  // Ensure the MSAL service is initialized before proceeding.
  await firstValueFrom(msalService.initialized);

Edit 2: This issue was due to an oversight on my part, the msal-service was using a const instance instead of a the class variable. Changing const msalInstance = new PublicClientApplication(msalConfig); to: this.msalInstance = new PublicClientApplication(msalConfig); Fixed the issue.

1

There are 1 answers

0
Sampath On

Thanks @Low-Signal for sharing the solution in Qs and comment. Posting it as an answer for the benefit of community:

The error message "uninitialized_public_client_application" indicates that the MSAL PublicClientApplication instance has not been initialized before calling the handleRedirectPromise method. This can happen if you call handleRedirectPromise before the initialize method has finished executing.

Using the below code, I am able to use the Angular MSAL Library:

import { NgModule } from '@angular/core';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations'
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { MatListModule } from '@angular/material/list';


import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import {
  IPublicClientApplication,
  PublicClientApplication,
  BrowserCacheLocation,
  LogLevel,
  InteractionType,
} from '@azure/msal-browser';
import {
  MSAL_INSTANCE,
  MSAL_INTERCEPTOR_CONFIG,
  MsalInterceptorConfiguration,
  MSAL_GUARD_CONFIG,
  MsalGuardConfiguration,
  MsalBroadcastService,
  MsalService,
  MsalGuard,
  MsalRedirectComponent,
  MsalModule,
  MsalInterceptor,
} from '@azure/msal-angular';
import { BrowserModule } from '@angular/platform-browser';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';

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

const isIE =
  window.navigator.userAgent.indexOf('MSIE ') > -1 ||
  window.navigator.userAgent.indexOf('Trident/') > -1;

export function loggerCallback(logLevel: LogLevel, message: string) {
  console.log(message);
}

export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: 'xxxx-xxxx-xxxxx-xxxxx',
      authority: 'https://login.microsoftonline.com/tenantId',
      redirectUri: 'http://localhost:4200',
    },
    cache: {
      cacheLocation: BrowserCacheLocation.LocalStorage,
      storeAuthStateInCookie: isIE, // set to true for IE 11
    },
    system: {
      loggerOptions: {
        loggerCallback,
        logLevel: LogLevel.Info,
        piiLoggingEnabled: false,
      },
    },
  });
}

export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
  const protectedResourceMap = new Map<string, Array<string>>();
  protectedResourceMap.set(GRAPH_ENDPOINT, ['user.read']);

  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap,
  };
}

export function MSALGuardConfigFactory(): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    authRequest: {
      scopes: ['user.read'],
    },
  };
}
@NgModule({
  declarations: [AppComponent, HomeComponent, ProfileComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    MatButtonModule,
    MatToolbarModule,
    MatListModule,
    HttpClientModule,
    MsalModule,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true,
    },
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory,
    },
    {
      provide: MSAL_GUARD_CONFIG,
      useFactory: MSALGuardConfigFactory,
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: MSALInterceptorConfigFactory,
    },
    MsalService,
    MsalGuard,
    MsalBroadcastService,
  ],
  bootstrap: [AppComponent, MsalRedirectComponent],
})
export class AppModule {}

Output:

enter image description here

enter image description here

enter image description here