AzureAD / microsoft-authentication-library-for-js

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

`MSAL_GUARD_CONFIG` factory invoked before `APP_INITIALIZER` promise resolves with standalone component bootstrapping #6970

Open jefedeltodos opened 6 months ago

jefedeltodos commented 6 months ago

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

3.10.0

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

3.0.13

Public or Confidential Client?

Public

Description

This is a project that demonstrates that the MSAL_GUARD_CONFIG factory is called before the APP_INITIALIZER promise is resolved.

From Angular's documentation

The provided functions are injected at application startup and executed during app initialization. If any of these functions returns a Promise or an Observable, initialization does not complete until the Promise is resolved or the Observable is completed.

Steps to reproduce

  1. Clone: https://github.com/jefedeltodos/app_initializer_msal_issue
  2. Install the dependencies with npm i
  3. Start the application with npm start
  4. Open http://localhost:4200 in a browser.

Expected Results

  1. The APP_INITIALIZER is invoked and the promise is resolved.
  2. The MSAL_GUARD_CONFIG factory is invoked (along with the other MSAL components)
  3. The application loads successfully in the browser.

Actual Results

  1. The APP_INITIALIZER is invoked, but the Promise has not resolved.
  2. The MSAL_GUARD_CONFIG factory is invoked.
  3. An Error is thrown because the startup config values are unavailable because the APP_INITIALIZER promise hasn't yet resolved.
  4. The APP_INITIAIZER promise is resolved, and the config values are ready.
  5. The application does not load in the browser due to the previous error.

Notes

  1. This only appears to be an issue when the default route is protected with canActivate: [MsalGuard].
  2. If the default route is not protected, the application loads as expected.

Error Message

This issue arises early in the application bootstrapping before the MSAL gets involved. The problem is that the MSAL_GUARD_CONFIG factory method is being invoked before the promise of the APP_INITIALIZER has been resolved.

The error message that occurs is due to the environment-specific configuration values not being present because the MSAL bootstrapping happens before the APP_INITIALIZER promise resolves.

MSAL Logs

[vite] connecting... app.config.ts:11 appInit called app.config.ts:77 MSALGuardConfigFactory called core.mjs:6531 ERROR Error: Environment is uninitialized at _AppStartService.getEnv (app-start.service.ts:40:13) at Object.MSALGuardConfigFactory [as useFactory] (app.config.ts:78:37) at Object.factory (core.mjs:3322:38) at core.mjs:3219:47 at runInInjectorProfilerContext (core.mjs:866:9) at R3Injector.hydrate (core.mjs:3218:21) at R3Injector.get (core.mjs:3082:33) at injectInjectorOnly (core.mjs:1100:40) at ɵɵinject (core.mjs:1106:42) at Object.MsalGuard_Factory [as factory] (azure-msal-angular.mjs:339:34) handleError @ core.mjs:6531 Show 1 more frame Show less app-start.service.ts:29 got config: Object core.mjs:29749 Angular is running in development mode. client.ts:173 [vite] connected.

Network Trace (Preferrably Fiddler)

MSAL Configuration

{
  auth: {
    clientId: env.msalConfig.auth.clientId,
    authority: env.msalConfig.auth.authority,
    redirectUri: '/',
    postLogoutRedirectUri: '/'
  },
  cache: {
    cacheLocation: BrowserCacheLocation.LocalStorage
  },
  system: {
    allowNativeBroker: false, // Disables WAM Broker
    loggerOptions: {
      logLevel: LogLevel.Trace,
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
          default:
            console.log(message);
            return;
        }
      }
    }
  }
}

Relevant Code Snippets

import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
import { provideRouter, withDisabledInitialNavigation, withEnabledBlockingInitialNavigation } from '@angular/router';
import { routes } from './app.routes';
import { AppStartService } from './services/app-start.service';
import { ApiResponse, SpaEnvironment } from './models';
import { HTTP_INTERCEPTORS, provideHttpClient } from '@angular/common/http';
import { BrowserCacheLocation, BrowserUtils, IPublicClientApplication, InteractionType, LogLevel, PublicClientApplication } from '@azure/msal-browser';
import { MSAL_GUARD_CONFIG, MSAL_INSTANCE, MSAL_INTERCEPTOR_CONFIG, MsalBroadcastService, MsalGuard, MsalGuardConfiguration, MsalInterceptor, MsalInterceptorConfiguration, MsalService } from '@azure/msal-angular';

function appInitPromise(appStartService: AppStartService): () => Promise<ApiResponse<SpaEnvironment>> {
  console.info('appInit called');
  return () => appStartService.initPromise('./assets/config.json');
}

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

export function MSALInstanceFactory(appEnvironmentService: AppStartService): IPublicClientApplication {
  console.info('MSALInstanceFactory')

  const env = appEnvironmentService.getEnv();

  return new PublicClientApplication({
    auth: {
      clientId: env.msalConfig.auth.clientId,
      authority: env.msalConfig.auth.authority,
      redirectUri: '/',
      postLogoutRedirectUri: '/'
    },
    cache: {
      cacheLocation: BrowserCacheLocation.LocalStorage
    },
    system: {
      allowNativeBroker: false, // Disables WAM Broker
      loggerOptions: {
        loggerCallback,
        logLevel: LogLevel.Info,
        piiLoggingEnabled: false
      }
    }
  });
}

export function MSALInterceptorConfigFactory(appEnvironmentService: AppStartService): MsalInterceptorConfiguration {
  console.info('MSALInterceptorConfigFactory');

  const env = appEnvironmentService.getEnv();

  const protectedResourceMap = new Map<string, Array<string>>();
  protectedResourceMap.set(env.apiConfig.uri, env.apiConfig.scopes);

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

export function MSALGuardConfigFactory(appEnvironmentService: AppStartService): MsalGuardConfiguration {
  console.info('MSALGuardConfigFactory called');
  const env = appEnvironmentService.getEnv();
  return {
    interactionType: InteractionType.Redirect,
    authRequest: {
      scopes: [...env.apiConfig.scopes]
    },
    loginFailedRoute: '/login-failed'
  };
}

const initialNavigation = !BrowserUtils.isInIframe() && !BrowserUtils.isInPopup()
    ? withEnabledBlockingInitialNavigation() // Set to enabledBlocking to use Angular Universal
    : withDisabledInitialNavigation();

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    provideRouter(routes, initialNavigation),
    {
      provide: APP_INITIALIZER,
      useFactory: appInitPromise,
      multi: true,
      deps: [AppStartService],
    },
    // MSAL
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true,
    },
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory,
      deps: [AppStartService],
    },
    {
      provide: MSAL_GUARD_CONFIG,
      useFactory: MSALGuardConfigFactory,
      deps: [AppStartService],
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: MSALInterceptorConfigFactory,
      deps: [AppStartService],
    },
    MsalService,
    MsalGuard,
    MsalBroadcastService,
  ],
};

Reproduction Steps

  1. Clone https://github.com/jefedeltodos/app_initializer_msal_issue
  2. Install the dependencies with npm i
  3. Start the application with npm start
  4. Open http://localhost:4200 in a browser.

Expected Behavior

  1. The APP_INITIALIZER is invoked and the promise is resolved.
  2. The MSAL_GUARD_CONFIG factory is invoked (along with the other MSAL components)
  3. The application loads successfully in the browser.

Identity Provider

Entra ID (formerly Azure AD) / MSA

Browsers Affected (Select all that apply)

Chrome, Firefox, Edge, Safari

Regression

No response

Source

External (Customer)

jefedeltodos commented 6 months ago

I don't know if this is an MSAL issue, or a general Angular issue that should be opened with them.