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.?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With