user2531854
user2531854

Reputation: 866

APP_INITIALIZER is not firing before factory

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

Answers (4)

DiederikTiemstra
DiederikTiemstra

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

wassim-k
wassim-k

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

Avtandil Kavrelishvili
Avtandil Kavrelishvili

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

Fateh Mohamed
Fateh Mohamed

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

Related Questions