[email protected] or @azure/[email protected]@azure/[email protected]@azure/[email protected]@azure/[email protected]@azure/[email protected]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 Thanks, this information is really helpful! We'll make sure this gets documented.
Thanks for example.
PS: it is very important to use new Promise and not to use http.get
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;
}
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
}
]
};
}
}
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.tsimport { 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.
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).
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 馃槉
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]
}
})
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
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]
}
})