Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

APP_INITIALIZER and dependent token resolution issue

I am using APP_INITIALIZER for fetching config from json and it works fine. Earlier i was having an auth guard as part of application and it used to work great.

Then we split the authorization logic into a library and it works fine if we call forRoot() or if we give static values to config but to allow dynamic configuration I used the InjectionToken from library to provide config without calling forRoot.

The code for app.module.ts is like this:

let authConfig: any;

export function authConfigFactory() {
  return authConfig;
}

export function appInitializerFn(appConfigService: AppConfigService) {
  return () => {
    return appConfigService.loadAppConfig().then(() => {
      authConfig = {
        clientId: appConfigService.getConfig().CLIENT_ID,
        tokenEndpoint: appConfigService.getConfig().TOKEN_URL,
        redirectURL: "http://localhost",
      };
    });
  };
};

@NgModule({
.....
  imports: [
..
AuthLib
],
  providers: [
    AppConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: appInitializerFn,
      multi: true,
      deps: [AppConfigService]
    },
    AuthLibService,
    {
      provide: 'authConfig',
      useFactory: authConfigFactory,
      deps: []
    },
.....
]
bootstrap: [AppComponent]
})
export class AppModule { }

Now authConfigFactory is getting called way before appInitializerFn resulting in undefined and if i add async to authConfigFactory to prevent return statement till its defined then empty values are fed to AuthGuard resulting in invalid token url.

If i provide values manually in appInitializerFn before calling for promise the values gets translated and things work normally. But at that stage dynamic values are not present.

Code for app-config.service.ts:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { AppConfig } from '../_models/app-config';
import { LoggingService } from './logging.service';

@Injectable()
export class AppConfigService {
  static appConfig : AppConfig;
  private dataLoc: string;
  constructor(private http: HttpClient,
              private logger: LoggingService) { }

  loadAppConfig() {
      if(environment.production){
        this.dataLoc = '/assets/data/appConfig.json';
      }
      else{
        this.dataLoc = '/assets/data/appConfig-dev.json';
      }
    return new Promise<void>((resolve, reject) =>  {
      this.http.get(this.dataLoc).toPromise().then((response: AppConfig) => {
        AppConfigService.appConfig = <AppConfig>response;
        resolve();
      }).catch((response: any) => {
        reject(`Could not load file '${this.dataLoc}': 
${JSON.stringify(response)}`);
     });
    });
  }
  getConfig() {
    return AppConfigService.appConfig;
  }
}

Anything missing from library or code to make this work ?

I am bit new to Angular regime, let me know even in case i did some stupid mistake.

like image 453
jazz Avatar asked Jul 08 '19 05:07

jazz


1 Answers

So found the reason and solution. Posting it for anyone come stumbling to this question.

Since APP_INITIALIZER is getting invoked properly. The thing i missed was that i am using "HttpClient" to fetch the config which in-turn is invoking any HTTP_INTERCEPTORS defined in ROOT and eventually resulting in initialization of Authentication Service which in turn needs auth config as token injection in constructor.

Hence token is getting injected even before value are fetched causing it to go undefined/empty.

The solution was although pretty easy, either we can use

  1. HttpBackend to fetch json or
  2. Fetch the json in main.ts or
  3. Prevent interceptor to be invoked for that json path.

In my case point 3 was not possible as i want tight control on all communication, while Point 2 is bit clumsy. We went with approach 1. Sharing modified code for reference.

import { Injectable } from '@angular/core';
import { HttpClient, HttpBackend } from '@angular/common/http';
import { environment } from 'src/environments/environment';
import { AppConfig } from '../_models/app-config';
import { LoggingService } from './logging.service';

@Injectable()
export class AppConfigService {
  static appConfig : AppConfig;
  private dataLoc: string;
  constructor(private handler: HttpBackend,
              private logger: LoggingService) { }

  loadAppConfig() {
      if(environment.production){
        this.dataLoc = '/assets/data/appConfig.json';
      }
      else{
        this.dataLoc = '/assets/data/appConfig-dev.json';
      }
    return  new HttpClient(this.handler).get(this.dataLoc)
      .toPromise()
      .then((data:AppConfig) => {
        this.logger.info(data);
        AppConfigService.appConfig = data;
      });
  }
  getConfig() {
    return AppConfigService.appConfig;
  }
}

Thanks to @ysf, your minimal example gave me the idea that if its working in general then something else is invoking the authconfig during intialization.

like image 135
jazz Avatar answered Nov 16 '22 02:11

jazz