Reputation: 866
I'm using APP_INITIALIZER
to load environment specific variables. I need to use these variables inside of my authConfigFactory
, but the factory keeps loading before APP_INITIALIZER
has completed inside of the app config.
I'm using this library: https://github.com/manfredsteyer/angular-oauth2-oidc
I want to use the value of APP_CONFIG.authConfig.allowedUrls
inside of my auth config factory. How can I make sure it sets the configuration first before the auth factory.
I get this error in the factory : Cannot read property 'authConfig' of undefined
app.module.ts
:
providers: [
AppConfigService,
{
provide: APP_INITIALIZER,
useFactory: (config: AppConfigService) => () => config.load(),
multi: true,
deps: [AppConfigService]
},
{
provide: OAuthModuleConfig,
useFactory: authConfigFactory
}
]
app.config.ts
:
export let APP_CONFIG: AppConfig;
@Injectable()
export class AppConfigService {
constructor(
private injector: Injector
) {}
config = null;
public load() {
const http = this.injector.get(HttpClient);
return http
.get('../assets/config/config.json')
.pipe(
tap(returnedConfig => {
const t = new AppConfig();
APP_CONFIG = Object.assign(t, returnedConfig);
})
)
.toPromise();
}
}
auth-config-factor
:
export function authConfigFactory(): OAuthModuleConfig {
return {
resourceServer: {
allowedUrls: APP_CONFIG.authConfig.allowedUrls,
sendAccessToken: true
}
};
}
Upvotes: 13
Views: 5012
Reputation: 355
I had the same problem. The OAuthModuleConfig needs to be set synchronously. So the settings need to be loaded before the OAuthModuleConfig is created (in a factory).
I implemented it by loading the settings before the AppModule is bootstrapped.
Main.ts:
fetch('/assets/config.json')
.then(response => response.json())
.then(config => {
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic([{ provide: APP_SETTINGS, useValue: config }])
.bootstrapModule(AppModule)
.catch(err => console.error(err));
});
App.module.ts
Inside the module declaration:
....
providers: [
{ provide: OAuthModuleConfig, useFactory: authConfigFactory, deps: [APP_SETTINGS] },
],
bootstrap: [AppComponent]
.....
export function authConfigFactory(settings: AppSettings): OAuthModuleConfig {
return {
resourceServer: {
allowedUrls: settings.protectedUrls,
sendAccessToken: true,
}
};
}
Upvotes: 1
Reputation: 131
I came across this issue just today and the following is how I managed to fix it. hoping this might be helpful to someone.
This is specific to angular-oauth2-oidc
in a way
export const oauthModuleConfigFactory = (config: AppConfig): OAuthModuleConfig => ({
resourceServer: {
allowedUrls: [config.apiUrl],
sendAccessToken: true
}
});
// OAuthModuleConfig is provided by OAuthModule as useValue
// and Angular resolves useValue providers before APP_INITIALIZER
// This may be a bug, since Angular still attempts to resolve the value
// even if it has been overriden with a useFactory as below
const fixForAppInitializers = ({ ngModule, providers = [] }: ModuleWithProviders<any>) => ({
ngModule,
providers: [...providers.filter((value: any) => value.provide !== OAuthModuleConfig)]
});
@NgModule({
imports: [fixForAppInitializers(OAuthModule.forRoot())],
providers: [
AuthGuard,
{ provide: OAuthModuleConfig, useFactory: oauthModuleConfigFactory, deps: [APP_CONFIG] },
...
]
})
export class AuthModule { }
That wasn't enough though, because OAuthModule
's DefaultOAuthInterceptor
requires OAuthModuleConfig
which needs the app.config.json
to load first, but in order to load the config we need to use the HttpClient
which injects HTTP_INTERCEPTORS
on request, thus throwing config is not loaded
error below.
This is how I solved that problem:
// We're using the primitive HttpXhrBackend service instead of HttpClient
// so that it does not attempt to inject http interceptors
// which has app config as a dependency, thus attempting to inject the config
// before it's loaded and throwing an error in the process
@Injectable()
export class AppConfigLoader {
private _config: AppConfig | undefined;
public constructor(
private readonly httpClient: HttpXhrBackend
) { }
public get config(): AppConfig {
if (!this._config) {
throw new Error('config is not loaded');
}
return this._config;
}
public async load(): Promise<void> {
const request = this.createJsonRequest<AppConfig>(`${window.location.origin}/app.config.json`);
this._config = await this.httpClient.handle(request).pipe(
filter(event => event instanceof HttpResponse),
map((response: any) => response.body)
).toPromise();
}
private createJsonRequest<T>(url: string): HttpRequest<T> {
return new HttpRequest<T>('GET', url, { headers: new HttpHeaders({ 'content-type': 'application/json' }) });
}
}
Upvotes: 1
Reputation: 1757
I have same problem and fix it.
module-factory:
import { InjectionToken } from '@angular/core';
import { HttpClient, HttpBackend } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { AuthorizationService } from './authorization.service';
export const AUTH_CONFIG = new InjectionToken<string>('auth.config.path', {
factory: () => 'config.json',
});
export function CheckAuthorizationState(handler: HttpBackend, authService: AuthorizationService, path: string) {
return async () => {
const http = new HttpClient(handler);
// get config for authorization
await http.get(path)
.pipe(
map((response: any) => {
authService.init(response);
})
).toPromise();
// check authorization
await authService.checkAuthorization();
};
}
module:
import { NgModule, APP_INITIALIZER, ModuleWithProviders, InjectionToken } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthorizationService } from './authorization.service';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthorizationGuardService } from './authorization-guard.service';
import { AuthorizationInterceptor } from './authorization-interpretator';
import { HttpBackend, HttpClient } from '@angular/common/http';
import { map } from 'rxjs/operators';
import { CheckAuthorizationState, AUTH_CONFIG } from './module-factories';
@NgModule({
imports: [
CommonModule
],
providers: [
AuthorizationService,
AuthorizationGuardService,
{
provide: APP_INITIALIZER,
useFactory: CheckAuthorizationState,
deps: [HttpBackend, AuthorizationService, AUTH_CONFIG],
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthorizationInterceptor,
multi: true
}
],
declarations: []
})
export class AuthorizationModule {
static forRoot(path: string): ModuleWithProviders {
return {
ngModule: AuthorizationModule,
providers: [
{ provide: AUTH_CONFIG, useValue: path }
]
};
}
}
Here is my authorization library which publish for you: https://www.npmjs.com/package/sso-authorization-oidc-client
Upvotes: 1
Reputation: 21367
I've had this problem before and tried many possibilities with no luck, the only solution is that i used ngrx/store
In app.config.ts you can dispatch an action to save the config in the store, and you can get it after that in other services by doing: store.select() Subscribe to it and do your control
app.module.ts
providers: [
AuthService, // oidc-client.ts where i need the config from json
DataService,
ConfigService,
{
provide: APP_INITIALIZER,
useFactory: loadConfig,
deps: [ConfigService],
multi: true,
},
config.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { IAppStore } from '../models/store.model';
import * as ConfigActions from '../store/actions/config.actions';
@Injectable()
export class ConfigService {
public config: any = {};
constructor(private http: HttpClient, private store: Store<IAppStore>) {}
getConfig(key: string): string {
return this.config[key] || '';
}
public loadConfig() {
return new Promise((resolve, reject) => {
this.http.get('app-config.json').subscribe(
(response) => {
this.config = response;
this.store.dispatch(new ConfigActions.AddConfig(response)); // dispatch action to update the store
resolve(true);
}
);
});
}}
AuthService
import { Log, User, UserManager, WebStorageStateStore } from 'oidc-client';
...
@Injectable()
export class AuthService {
private _userManager: UserManager;
public _user: User;
constructor(
private store: Store<IAppStore>
private httpClient: HttpClient,
private route: Router,
private configs: ConfigService
) {
this.store.select('appConfig').subscribe((configdata) => {
Log.logger = console;
const config = {
authority: configdata.stsAuthority,
client_id: configdata.clientId,
redirect_uri: `${configdata.clientRoot}/#/auth-callback#`,
scope: 'openid profile fusionApi.full_access',
response_type: 'id_token token',
post_logout_redirect_uri: `${configdata.clientRoot}?postLogout=true`, // delet all stored tokens after logout
userStore: new WebStorageStateStore({ store: window.localStorage }),
automaticSilentRenew: true,
silent_redirect_uri: `${configdata.clientRoot}/assets/html/silent-refresh-redirect.html`,
};
this._userManager = new UserManager(config);
this._userManager.getUser().then((user) => {
if (user && !user.expired) {
this._user = user;
}
});
...
}
login(): Promise<any> {
return this._userManager.signinRedirect();
}
...
Upvotes: 3