Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular imported modules do not wait for APP_INITIALIZER

Tags:

angular

auth0

I'm trying to use the auth0/auth0-angular library in an Angular 11 app.

I'm following the section on loading config dynamically.

It provides this example app module code:

// app.module.ts
// ---------------------------
import { AuthModule, AuthClientConfig } from '@auth0/auth0-angular';

// Provide an initializer function that returns a Promise
function configInitializer(
  handler: HttpBackend,
  config: AuthClientConfig
) {
  return () =>
    new HttpClient(handler)
      .get('/config')
      .toPromise()
      .then((loadedConfig: any) => config.set(loadedConfig));   // Set the config that was loaded asynchronously here
}

// Provide APP_INITIALIZER with this function. Note that there is no config passed to AuthModule.forRoot
imports: [
  // other imports..

  HttpClientModule,
  AuthModule.forRoot(),   //<- don't pass any config here
],
providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: configInitializer,    // <- pass your initializer function here
    deps: [HttpBackend, AuthClientConfig],
    multi: true,
  },
],

In short, it uses an APP_INITIALIZER provider to dynamically load config via a Promise, and this should complete before the Auth0 library's AuthModule is instantiated, so that it has the appropriate Auth0 config values loaded from an API and AuthClientConfig.set(...) has been called with those values in advance.

The Angular APP_INITIALIZER documentation says:

If any of these functions returns a Promise, initialization does not complete until the Promise is resolved.

So, their example makes sense on the face of it.

However, when I try to actually implement this solution in my own app I get the following error:

Error: Configuration must be specified either through AuthModule.forRoot or through AuthClientConfig.set

This suggests that the AuthModule has been instantiated before the config has been loaded and set.

It seems to me that Angular is not actually waiting for the Promise to resolve before it begins instantiating imported modules.

I think that this StackBlitz demo demonstrates the problem in a simplified example without any of the Auth0 dependencies.

In this example, I would expect that TestModule is not instantiated until after the Promise has resolved, so I should see the following console output:

Inside factory method
Inside promise
Inside timeout
TestModule constructor

But what I actually see is this:

TestModule constructor
Inside factory method
Inside promise
Inside timeout

Could someone please help me to understand the exact nature of APP_INITIALIZER, i.e. when is it called, when does Angular wait for the Promise to resolve, when does Angular begin instantiating other modules, why might my Auth0 setup not be loading correctly, etc.?

like image 490
Sam Williams Avatar asked Mar 29 '21 18:03

Sam Williams


1 Answers

TL;DR - I ended up solving this by loading the config in main.ts before bootstrapping the application and then making the config available via a custom injection token and then my app config service doesn't need to wait for it to load via HTTP as it's already available.

The details

A snippet of my AppConfig interface:

export interface AppConfig {
  auth: {
    auth0_audience: string,
    auth0_domain: string,
    auth0_client_id: string,
  };
}

The custom InjectionToken in my constants file:

 const APP_CONFIG: InjectionToken<AppConfig>
  = new InjectionToken<AppConfig>('Application Configuration');

main.ts:

fetch('/config.json')
  .then(response => response.json())
  .then((config: AppConfig) => {
    if (environment.production) {
      enableProdMode();
    }

    platformBrowserDynamic([
      { provide: APP_CONFIG, useValue: config },
    ])
      .bootstrapModule(AppModule)
      .catch(err => console.error(err));
  });

And then in my main AppModule I import the Auth0 AuthModule.forRoot() without config and call my own AppConfigService to configure the AuthModule.

I still need the APP_INITIALIZER to depend on AppConfigService and to return a Promise which somehow makes Angular wait until the AppConfigService constructor has been called, but it otherwise doesn't do anything (and still doesn't delay AuthModule being initialised), so I just resolve it immediately.

AppModule:

@NgModule({
  declarations: [
    ...
  ],
  imports: [
    AuthModule.forRoot(),
    ...
  ],
  providers: [
    AppConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: () => () => {
        return new Promise(resolve => {
          resolve();
        });
      },
      deps: [ AppConfigService ],
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthHttpInterceptor,
      multi: true,
    },
  ],
  bootstrap: [ AppComponent ],
})
export class AppModule { }

Finally, the AppConfigService:

@Injectable()
export class AppConfigService {

  constructor(
    @Inject(APP_CONFIG) private readonly appConfig: AppConfig,
    private authClientConfig: AuthClientConfig,
  ) {
    this.authClientConfig.set({
      clientId: this.appConfig.auth.auth0_client_id,
      domain: this.appConfig.auth.auth0_domain,
      audience: this.appConfig.auth.auth0_audience,
      httpInterceptor: {
        allowedList: [
          ...
        ],
      },
    });
  }
}

This all seems to work fine, although I still don't understand the exact nature of APP_INITIALIZER and I'm not very happy calling the Auth0 client config's set method in a constructor rather than an asynchronous "load" method like the documentation suggests.

like image 160
Sam Williams Avatar answered Sep 20 '22 14:09

Sam Williams