How to initialize msal configurations using APP_INITIALIZER

See original GitHub issue

Library

  • msal@1.x.x or @azure/msal@1.x.x
  • @azure/msal-browser@2.x.x
  • @azure/msal-angular@0.x.x
  • @azure/msal-angular@1.x.x
  • @azure/msal-angularjs@1.x.x

Documentation location

  • docs.microsoft.com
  • MSAL.js Github Wiki
  • README file
  • Other (please fill in)
  • Documentation does not exist

Description

I found that most of the developers are facing issues in loading msal configurations from a config service/ app settings service, Even i struggled for couple of days. But finally I was able to achieve this.

Here is my solution:

config.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient, HttpBackend } from '@angular/common/http';
import { map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class ConfigService {

  private settings: any;
  private http: HttpClient;
  constructor(private readonly httpHandler: HttpBackend) {
    this.http = new HttpClient(httpHandler);
  }

  init(endpoint: string): Promise<boolean> {
    return new Promise<boolean>((resolve, reject) => {
      this.http.get(endpoint).pipe(map(res => res))
        .subscribe(value => {
          this.settings = value;
          resolve(true);
        },
        (error) => {
          reject(error);
        });
    });
  }

  getSettings(key?: string | Array<string>): any {
    if (!key || (Array.isArray(key) && !key[0])) {
      return this.settings;
    }

    if (!Array.isArray(key)) {
      key = key.split('.');
    }

    let result = key.reduce((acc: any, current: string) => acc && acc[current], this.settings);

    return result;
  }
}

Note config.service.ts, constructor, in this we are not injecting HttpClient, because if you inject HttpClient then angular first resolve all the HTTP_INTERCEPTORS, and when you use MsalInterceptor in app module, this makes angular to load MsalService and other component used by Msalinterceptor load before APP_INITIALIZER. To resolve this issue we need to by pass HTTP_INTERCEPTORS, so for this we can use HttpBackend handler, and then create local instance of HttpClient in config service constructor. This will bypass the HTTP_INTERCEPTORS, while getting config file.

msal-application.module.ts

import { InjectionToken, NgModule, APP_INITIALIZER } from '@angular/core';
import {
    MSAL_CONFIG,
    MSAL_CONFIG_ANGULAR,
    MsalAngularConfiguration
    , MsalService, MsalModule, MsalInterceptor
  } from '@azure/msal-angular';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ConfigService } from '../config.service';
import { Configuration } from 'msal';

const AUTH_CONFIG_URL_TOKEN = new InjectionToken<string>('AUTH_CONFIG_URL');

export function initializerFactory(env: ConfigService, configUrl: string): any {
    // APP_INITIALIZER, except a function return which will return a promise
    // APP_INITIALIZER, angular doesnt starts application untill it completes
    const promise = env.init(configUrl).then((value) => {
        console.log(env.getSettings('clientID'));
    });
    return () => promise;
}

export function msalConfigFactory(config: ConfigService): Configuration {
    const auth = {
        auth: {
            clientId: config.getSettings('clientID'),
            authority: config.getSettings('authority'),
            redirectUri: config.getSettings('redirectUri')
        },
        cache: {
            cacheLocation: config.getSettings('cacheLocation')
        }
    };
    return (auth as Configuration);
}

export function msalAngularConfigFactory(config: ConfigService): MsalAngularConfiguration {
    const auth = {
        unprotectedResources: config.getSettings('unprotectedResources'),
        protectedResourceMap: config.getSettings('protectedResourceMap'),
    };
    return (auth as MsalAngularConfiguration);
}

@NgModule({
    providers: [
    ],
    imports: [MsalModule]
})
export class MsalApplicationModule {

    static forRoot(configFile: string) {
        return {
            ngModule: MsalApplicationModule,
            providers: [
                ConfigService,
                { provide: AUTH_CONFIG_URL_TOKEN, useValue: configFile },
                { provide: APP_INITIALIZER, useFactory: initializerFactory,
                     deps: [ConfigService, AUTH_CONFIG_URL_TOKEN], multi: true },
                {
                    provide: MSAL_CONFIG,
                    useFactory: msalConfigFactory,
                    deps: [ConfigService]
                },
                {
                    provide: MSAL_CONFIG_ANGULAR,
                    useFactory: msalAngularConfigFactory,
                    deps: [ConfigService]
                },
                MsalService,
                {
                    provide: HTTP_INTERCEPTORS,
                    useClass: MsalInterceptor,
                    multi: true
                }
            ]
        };
    }
}

Create a config.json file:

{
    "clientID": "xxxx",
    "authority": "https://login.microsoftonline.com/xxxx",
    "redirectUri": "http://localhost:4200/",
    "cacheLocation": "localStorage",
    "protectedResourceMap": [
        ["xxxxxx", ["xxxxxx/.default"]]
    ], 
    "extraQueryParameters": "xxxxx"
}

Now use this MsalApplicationModule in app.module.ts file, imports section as:

MsalApplicationModule.forRoot('config.json')

Now use MsalService in app.component.ts file as per the sample provided by the authors of this library.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:4
  • Comments:9 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
szymon-wesolowskicommented, Aug 12, 2020

Thank you for suggestion, I handle it exactly the same as in settings loader, I have replace existing translation loader with my own.

export function customTranslateLoader(httpHandler: HttpBackend): TranslateLoader {
  const http = new HttpClient(httpHandler)
  return translatePartialLoader(http);
}
TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: customTranslateLoader,
        deps: [HttpBackend]
      },
      missingTranslationHandler: {
        provide: MissingTranslationHandler,
        useFactory: missingTranslationHandler,
        deps: [JhiConfigService]
      }
    })
1reaction
szymon-wesolowskicommented, Aug 11, 2020

Hello, I have spent few days trying to make it work, but all the time msal config factory methods are invoked before the config file is recived. Where is the crucial part which wait until promise from config factory is returned? I am using Angular 9 and “@azure/msal-angular”: “^1.0.0-beta.5” app.settings.service.ts


import { HttpClient, HttpBackend } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { map } from 'rxjs/operators';

export interface IAppSettings {
  domain?: string
}

@Injectable({ providedIn: 'root' })
export class AppSettingsService {
  appSettings: IAppSettings = {};
  http: HttpClient;

  constructor(private readonly httpHandler: HttpBackend) {
    this.http = new HttpClient(httpHandler)
  }

  load(): Promise<void> {
    let appSettingsUrl: string;
    if (process.env.NODE_ENV !== "local") {
      appSettingsUrl = "/appSettings.php"
    } else {
      appSettingsUrl = "https://xxx/appSettings.php"
    }

    return new Promise<void>((resolve, reject) => {
      this.http.get<IAppSettings>(appSettingsUrl).pipe(map(res => res))
        .subscribe(appSettings => {
          this.appSettings = appSettings;
          resolve();
        },
          (error) => {
            reject(error);
          });
    });
  }
}

msal-application.module.ts

import { NgModule, APP_INITIALIZER, InjectionToken } from '@angular/core';
import {
    MSAL_CONFIG,
    MSAL_CONFIG_ANGULAR,
    MsalAngularConfiguration,
    MsalInterceptor,
    MsalModule,
    MsalService
} from '@azure/msal-angular';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { Configuration } from 'msal';
import { AppSettingsService } from './app.settings.service';

const isIE = window.navigator.userAgent.includes("MSIE ") || window.navigator.userAgent.includes("Trident/");

function MSALConfigFactory(appSettingsService: AppSettingsService): Configuration {
    return {
        auth: {
            clientId: process.env.clientId!,
            authority: `https://${appSettingsService.appSettings.domain}.b2clogin.com/${appSettingsService.appSettings.domain}.onmicrosoft.com/B2C_1_signin1`,
            validateAuthority: false,
            redirectUri: process.env.redirectUri,
            postLogoutRedirectUri: process.env.postLogoutRedirectUri,
            navigateToLoginRequestUrl: true,
        },
        cache: {
            cacheLocation: "localStorage",
            storeAuthStateInCookie: isIE, // set to true for IE 11
        }
    };
}

function MSALAngularConfigFactory(appSettingsService: AppSettingsService): MsalAngularConfiguration {
    return {
        popUp: !isIE,
        consentScopes: [
            `https://${appSettingsService.appSettings.domain}.onmicrosoft.com/api/all`
        ],
        unprotectedResources: ['i18n'],
        protectedResourceMap: [
            [process.env.apiUrl!, [`https://${appSettingsService.appSettings.domain}.onmicrosoft.com/api/all`]]
        ],
        extraQueryParameters: {}
    };
}

function appSettingsLoader(appSettingsService: AppSettingsService): any {
    const promise = appSettingsService.load().then(() => {
    });
    return () => promise;
}

@NgModule({
    providers: [
    ],
    imports: [MsalModule]
})
export class MsalApplicationModule {
    // eslint-disable-next-line
    static forRoot() {
        return {
            ngModule: MsalApplicationModule,
            providers: [
                AppSettingsService,

                {
                    provide: APP_INITIALIZER,
                    useFactory: appSettingsLoader,
                    deps: [AppSettingsService,],
                    multi: true
                },
                {
                    provide: MSAL_CONFIG,
                    useFactory: MSALConfigFactory,
                    deps: [AppSettingsService]
                },
                {
                    provide: MSAL_CONFIG_ANGULAR,
                    useFactory: MSALAngularConfigFactory,
                    deps: [AppSettingsService]
                },
                MsalService,
                {
                    provide: HTTP_INTERCEPTORS,
                    useClass: MsalInterceptor,
                    multi: true
                }
            ]
        };
    }
}
Read more comments on GitHub >

github_iconTop Results From Across the Web

Angular APP_INITIALIZER in module not working properly
I'm using APP_INITIALIZER to block the init of the app and get configuration for MSAL-ANGULAR. The issue is only with Firefox. The app...
Read more >
Dynamically loading MSAL configuration Angular SPA - Medium
There is a proposed solution on that thread about using APP INITIALIZER to coordinate fetching your configuration using an API.
Read more >
Initialize MSAL.js client apps - Microsoft Entra
Initialize the MSAL.js authentication context by instantiating a PublicClientApplication with a Configuration object.
Read more >
APP_INITIALIZER - Angular
The following example illustrates how to configure a multi-provider using APP_INITIALIZER token and a function returning a promise. content_copy
Read more >
Angular How to use APP_INITIALIZER - TekTutorialsHub
This gives us an opportunity to hook into the initialization process and run some our application custom logic. You can load runtime configuration...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found