Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to initialize msal configurations using APP_INITIALIZER #1403

Closed
2 of 10 tasks
vinusorout opened this issue Mar 21, 2020 · 9 comments · Fixed by #1480
Closed
2 of 10 tasks

How to initialize msal configurations using APP_INITIALIZER #1403

vinusorout opened this issue Mar 21, 2020 · 9 comments · Fixed by #1480
Assignees
Labels
documentation Related to documentation. msal-angular Related to @azure/msal-angular package question Customer is asking for a clarification, use case or information.

Comments

@vinusorout
Copy link
Contributor

vinusorout commented Mar 21, 2020

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.

@vinusorout vinusorout added documentation Related to documentation. question Customer is asking for a clarification, use case or information. labels Mar 21, 2020
@jasonnutter jasonnutter self-assigned this Mar 23, 2020
@jasonnutter jasonnutter added the msal-angular Related to @azure/msal-angular package label Mar 23, 2020
@jasonnutter
Copy link
Contributor

@vinusorout Thanks, this information is really helpful! We'll make sure this gets documented.

@vitaliidasaev
Copy link

Thanks for example.
PS: it is very important to use new Promise and not to use http.get.toPromise
My code block:

export function loadSettingsFactoryProvider(settingsService: SettingsService) {
  console.log('loadSettingsFactoryProvider');

  const baseSettings$ = settingsService.getSettings().pipe(
    tap((settings: Settings) => {
      console.log('loadSettingsFactoryProvider', settings);
      settingsService.settings = Object.freeze(settings);
    }),
    shareReplay(1)
  );

  const promise: Promise<any> = new Promise<boolean>((resolve, reject) => {
    baseSettings$.subscribe(
      (value) => {
        console.log('resolve', value);
        resolve(true);
      },
      (error) => {
        console.error('reject', error);
        reject(error);
      }
    );
  });

  return () => promise;
}

@szymon-wesolowski
Copy link

szymon-wesolowski commented 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
                }
            ]
        };
    }
}

@vinusorout
Copy link
Contributor Author

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

This seems to be fine, please check all your dependencies, I think some other services or modules is initializing HttpClient before resolving appSettingsLoader.

@calmez
Copy link

calmez commented Aug 12, 2020

I'm also having trouble getting this to work. On my end it looks like the request is not even made. Instead the error handler of the subscribe call gets invoked with an error that happens when I try to access AppSettingsService's settings (in the factory functions for MSAL) which are of course not yet loaded.
What I expected was that these factory functions only get called once AppSettingsService is completely initialized (aka. the promise is resolved).

@calmez
Copy link

calmez commented Aug 12, 2020

Alright, I found what was the problem that I had. I was injecting the HttpClient instead creating it with an injected backend like you do. After I changed that it started to work.
However, that does not seem to be @szymon-wesolowski 's problem, but maybe it helps somebody else 😊

@szymon-wesolowski
Copy link

szymon-wesolowski commented Aug 12, 2020

This seems to be fine, please check all your dependencies, I think some other services or modules is initializing HttpClient before resolving appSettingsLoader.

As a confirmation I have removed MsalInterceptor from providers.

// {
//     provide: HTTP_INTERCEPTORS,
//     useClass: MsalInterceptor,
//     multi: true
// }

After that it start working. @vinusorout I will try to find where HttpClient is initialized.
I have found it, but I dont know how to solve it becouse translatePartialLoader is from external library.

TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: translatePartialLoader,
        deps: [HttpClient]
      },
      missingTranslationHandler: {
        provide: MissingTranslationHandler,
        useFactory: missingTranslationHandler,
        deps: [JhiConfigService]
      }
    })

@vinusorout
Copy link
Contributor Author

vinusorout commented Aug 12, 2020

This seems to be fine, please check all your dependencies, I think some other services or modules is initializing HttpClient before resolving appSettingsLoader.

As a confirmation I have removed MsalInterceptor from providers.

// {
//     provide: HTTP_INTERCEPTORS,
//     useClass: MsalInterceptor,
//     multi: true
// }

After that it start working. @vinusorout I will try to find where HttpClient is initialized.
I have found it, but I dont know how to solve it becouse translatePartialLoader is from external library.

TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: translatePartialLoader,
        deps: [HttpClient]
      },
      missingTranslationHandler: {
        provide: MissingTranslationHandler,
        useFactory: missingTranslationHandler,
        deps: [JhiConfigService]
      }
    })

You can create two modules Login n Home.
Login Module: In app module set this as startup module and inject masl in this module perform login and then navigate to Home module
Home Module: load other dependencies like TranslateModule

Note: Use lazy loading for modules

@szymon-wesolowski
Copy link

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 27, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
documentation Related to documentation. msal-angular Related to @azure/msal-angular package question Customer is asking for a clarification, use case or information.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants