15

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.?

Sam Williams
  • 565
  • 4
  • 11

3 Answers3

19

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.

Sam Williams
  • 565
  • 4
  • 11
  • 3
    Hey mate, thanks for this. I spent all day, trying to get an APP_INITIALIZER to fire (with no success). Using the main.ts method, I was able to Inject the AppConfig class. I didn't need the APP_INITIALIZER step as the config gets injected straight into the Auth0AuthService, which then uses it to create the WebAuth client. – Adam Hardy Apr 06 '21 at 06:41
  • 1
    @AdamHardy Do you have maybe any snippet for your solution? – AvgustinTomsic Jun 07 '22 at 09:23
0

I think you might need to wrap that api call in a Promise.

function configInitializer(handler: HttpBackend, config: AuthClientConfig) {
  return () => fetchAndSetConfig(handler, config);
}

function fetchAndSetConfig() {
  return new Promise((resolve, reject) => {
     new HttpClient(handler).get('/config').toPromise()
       .then((loadedConfig: any) => {
          config.set(loadedConfig);
          resolve(true);
      }); 
  })
}
nsquires
  • 899
  • 2
  • 8
  • 20
  • 1
    Thanks for the suggestion. Then you just have a promise waiting on another promise. Angular should wait on the outer promise which is only resolved when the inner promise completes and resolves it. I tried your approach with my main project and with the StackBlitz example and it still doesn't work. – Sam Williams Mar 30 '21 at 19:11
0

I had the same problem with the dynamic auth0 config. I've tried this solution by Philip Lysenko. The main idea is to use lazy loading for the main route. It works for me.

And one more, set initialNavigation: 'enabledNonBlocking' for your root router configuration.

How it looks on the Network tab: picture

Blunderchips
  • 534
  • 4
  • 22
Dmytro
  • 1
  • 1
  • Thanks for the suggestion, @Dmytro. I took another approach in the end because I wasn't comfortable with the lazy loading approach - it seemed like a large structural change that was a bit overkill for this scenario. – Sam Williams Apr 05 '21 at 17:46