Angular-auth-oidc-client: No Provider for UrlService when using in Lazy Loaded Module

Created on 5 May 2020  路  14Comments  路  Source: damienbod/angular-auth-oidc-client

I was using version 10.0.15 and I tried to upgrade to 11.

Previously I was lazy loading the auth module as logging on in our app is optional (you can create a logon to get extra features if you wish).

Previously I was doing this.

import { NgModule } from '@angular/core';
import {
  AuthModule,
  AuthWellKnownEndpoints,
  OidcSecurityService,
  OpenIdConfiguration
} from 'angular-auth-oidc-client';
import { environment } from '../../../environments/environment';
import { CanActivateService } from './can-activate.service';
import { OAuthService } from './oauth.service';

@NgModule({
  imports: [AuthModule.forRoot()],
  providers: [CanActivateService, OAuthService]
})

export class OAuthModule {

  readonly #oidcSecurityService: OidcSecurityService;

  constructor(
    oidcSecurityService: OidcSecurityService
  ) {

    this.#oidcSecurityService = oidcSecurityService;
    const config: OpenIdConfiguration = {
      stsServer: environment.clientSettings.stsServer,
      redirect_url: environment.clientSettings.redirect_url,
      client_id: environment.clientSettings.client_id,
      response_type: environment.clientSettings.response_type,
      scope: environment.clientSettings.scope,
      post_logout_redirect_uri: environment.clientSettings.post_logout_redirect_uri,
      start_checksession: environment.clientSettings.start_checksession,
      silent_renew: environment.clientSettings.silent_renew,
      silent_renew_url: environment.clientSettings.silent_renew_url,
      post_login_route: environment.clientSettings.post_login_route,
      // HTTP 403
      forbidden_route: environment.clientSettings.forbidden_route,
      // HTTP 401
      unauthorized_route: environment.clientSettings.unauthorized_route,
      log_console_warning_active: environment.clientSettings.log_console_warning_active,
      log_console_debug_active: environment.clientSettings.log_console_debug_active,
      max_id_token_iat_offset_allowed_in_seconds: environment.clientSettings.max_id_token_iat_offset_allowed_in_seconds
    };

    const authWellKnownEndpoints: AuthWellKnownEndpoints = {
      issuer: environment.authWellKnownEndpoints.issuer,
      jwks_uri: environment.authWellKnownEndpoints.jwks_uri,
      authorization_endpoint: environment.authWellKnownEndpoints.authorization_endpoint,
      token_endpoint: environment.authWellKnownEndpoints.token_endpoint,
      userinfo_endpoint: environment.authWellKnownEndpoints.userinfo_endpoint,
      end_session_endpoint: environment.authWellKnownEndpoints.end_session_endpoint,
      check_session_iframe: environment.authWellKnownEndpoints.check_session_iframe,
      revocation_endpoint: environment.authWellKnownEndpoints.revocation_endpoint,
      introspection_endpoint: environment.authWellKnownEndpoints.introspection_endpoint
    };
    this.#oidcSecurityService.setupModule(config, authWellKnownEndpoints);
  }
}

In the new version I tried

import { NgModule } from '@angular/core';
import {
  AuthModule,
  AuthWellKnownEndpoints,
  OidcSecurityService,
  OpenIdConfiguration,
  LogLevel
} from 'angular-auth-oidc-client';
import { environment } from '../../../environments/environment';
import { CanActivateService } from './can-activate.service';
import { OAuthService } from './oauth.service';

@NgModule({
  imports: [AuthModule.forRoot()],
  providers: [CanActivateService, OAuthService]
})

export class OAuthModule {

  constructor(
    oidcSecurityService: OidcSecurityService,
  ) {

    const config: OpenIdConfiguration = {
      stsServer: environment.clientSettings.stsServer,
      redirectUrl: environment.clientSettings.redirect_url,
      clientId: environment.clientSettings.client_id,
      responseType: environment.clientSettings.response_type,
      scope: environment.clientSettings.scope,
      postLogoutRedirectUri: environment.clientSettings.post_logout_redirect_uri,
      startCheckSession: environment.clientSettings.start_checksession,
      silentRenew: environment.clientSettings.silent_renew,
      silentRenewUrl: environment.clientSettings.silent_renew_url,
      postLoginRoute: environment.clientSettings.post_login_route,
      // HTTP 403
      forbiddenRoute: environment.clientSettings.forbidden_route,
      // HTTP 401
      unauthorizedRoute: environment.clientSettings.unauthorized_route,
      logLevel: LogLevel.Error,
      maxIdTokenIatOffsetAllowedInSeconds: environment.clientSettings.max_id_token_iat_offset_allowed_in_seconds
    };

    const authWellKnownEndpoints: AuthWellKnownEndpoints = {
      issuer: environment.authWellKnownEndpoints.issuer,
      jwksUri: environment.authWellKnownEndpoints.jwks_uri,
      authorizationEndpoint: environment.authWellKnownEndpoints.authorization_endpoint,
      tokenEndpoint: environment.authWellKnownEndpoints.token_endpoint,
      userinfoEndpoint: environment.authWellKnownEndpoints.userinfo_endpoint,
      endSessionEndpoint: environment.authWellKnownEndpoints.end_session_endpoint,
      checkSessionIframe: environment.authWellKnownEndpoints.check_session_iframe,
      revocationEndpoint: environment.authWellKnownEndpoints.revocation_endpoint,
      introspectionEndpoint: environment.authWellKnownEndpoints.introspection_endpoint
    };
    oidcSecurityService.configuration.configuration = config;
    oidcSecurityService.configuration.wellknown = authWellKnownEndpoints;
  }
}

I get the following error when I load the module

instrument.js:110 ERROR Error: Uncaught (in promise): NullInjectorError: R3InjectorError(UserModule)[OAuthModule -> OidcSecurityService -> CallbackService -> CallbackService -> UrlService -> UrlService -> UrlService]: 
  NullInjectorError: No provider for UrlService!
NullInjectorError: R3InjectorError(UserModule)[OAuthModule -> OidcSecurityService -> CallbackService -> CallbackService -> UrlService -> UrlService -> UrlService]: 

All the sample use APP_INITIALIZER, which I cannot use being a lazy loading module.

Can you give me some guidance how to fix this, or do I need to put this back in the app module now?

investigate

Most helpful comment

Hey @stonecourier , you have to pass the config in the appmodule and register the authModule.forRoot() there. If you load the config from the server or get it static, that does not matter, you just have to pass the correct one in the withConfig method. The app.module can be normally accessible. In the Lazy loaded module then you can start the authentication process. I just added an example for lazy loading here https://github.com/damienbod/angular-auth-oidc-client/pull/711. Hope this helps!

All 14 comments

Hey @stonecourier , why don't you use the withConfig method as replacement to the setupModule before? Maybe you can give that a try.

Sorry, I tried using withConfig and passing in the configservice, but could not get it to work.

In the old version I could pass in a known endpoint rather than getting it from the server. I am unsure how to do this in the new version.

Hey @stonecourier , you have to pass the config in the appmodule and register the authModule.forRoot() there. If you load the config from the server or get it static, that does not matter, you just have to pass the correct one in the withConfig method. The app.module can be normally accessible. In the Lazy loaded module then you can start the authentication process. I just added an example for lazy loading here https://github.com/damienbod/angular-auth-oidc-client/pull/711. Hope this helps!

Thanks for that. Is there a way I can have angular-auth-oidc-client not being in the main module in the new code base? One of the benefits of the lazy load approach I was doing before is that the users don't have to load the code for it on startup (from memory the security dependency for angular-auth-oidc-client isn't that small).

Hey @stonecourier, I am afraid this is not possible in the current version. But I understand your usecase and we can tackle this in the upcoming versions. I boiled down your problem and can reproduce it. Will create a branch and test this.

I hope it is okay for you to close the issue, as loading with forRoot() is the preferred solution here. We will have this usecase on our list for the next releases. Please feel free to reopen. Thanks!

Thanks, hopefully a later release can cater for it. I will stick with 10.0.15 for now and look at the release notes for later versions when they come out.

Reopening beause we are working on a fix

You can check out the latest release 11.0.1. You can load the lib as documented also in a feature module. https://github.com/damienbod/angular-auth-oidc-client/blob/master/docs/features.md#using-the-oidc-package-in-a-module-or-a-angular-lib

I tried the latest version and followed what you suggested and it does not appear to fix the problem.

import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import {
  AuthModule,
  LogLevel,
  OidcConfigService
} from 'angular-auth-oidc-client';
import { environment } from '../../../environments/environment';
import { CanActivateService } from './can-activate.service';
import { OAuthService } from './oauth.service';

export function configureAuth(oidcConfigService: OidcConfigService) {
  const action$ = oidcConfigService.withConfig({
    stsServer: environment.clientSettings.stsServer,
    redirectUrl: environment.clientSettings.redirectUrl,
    clientId: environment.clientSettings.clientId,
    responseType: environment.clientSettings.responseType,
    scope: environment.clientSettings.scope,
    postLogoutRedirectUri: environment.clientSettings.postLogoutRedirectUri,
    startCheckSession: environment.clientSettings.startCheckSession,
    silentRenew: environment.clientSettings.silentRenew,
    silentRenewUrl: environment.clientSettings.silentRenewUrl,
    postLoginRoute: environment.clientSettings.postLoginRoute,
    forbiddenRoute: environment.clientSettings.forbiddenRoute,
    unauthorizedRoute: environment.clientSettings.unauthorizedRoute,
    logLevel: LogLevel.Error,
    maxIdTokenIatOffsetAllowedInSeconds: environment.clientSettings.maxIdTokenIatOffsetAllowedInSeconds

  });
  return () => action$;
}

@NgModule({
  imports: [
    AuthModule.forRoot(),
    HttpClientModule,
    CommonModule,
    RouterModule],
  providers: [
    {
      provide: APP_INITIALIZER,
      useFactory: configureAuth,
      deps: [OidcConfigService],
      multi: true
    },
    CanActivateService, OAuthService]
})

export class OAuthModule {

}

Error

ERROR Error: Uncaught (in promise): NullInjectorError: R3InjectorError(UserModule)[OAuthService -> OidcSecurityService -> CallbackService -> CallbackService -> UrlService -> UrlService -> UrlService]: 
  NullInjectorError: No provider for UrlService!
NullInjectorError: R3InjectorError(UserModule)[OAuthService -> OidcSecurityService -> CallbackService -> CallbackService -> UrlService -> UrlService -> UrlService]: 
  NullInjectorError: No provider for UrlService!
    at NullInjector.get (core.js:1085)
    at R3Injector.get (core.js:16875)
    at R3Injector.get (core.js:16875)
    at R3Injector.get (core.js:16875)
    at injectInjectorOnly (core.js:940)
    at 傻傻inject (core.js:950)
    at Object.CallbackService_Factory [as factory] (angular-auth-oidc-client.js:2696)

Hey @stonecourier

if you want to have multi line code blocks, please use the backticks three times in a separate row. This makes you code easier to read. I modified your comments accordingly.

I digged a bit into this and currently angular does not support something like ann APP_INITIALIZER on lazy loaded module level. (On a eager loaded module level the APP_INITIALIZER works but not an a lazy loaded one.)

Thread is here: https://github.com/angular/angular/issues/17606#issuecomment-523410215

The complete configuration in the previous versions was not optimal, so we improved it. With this the ability to _load_ it in lazy loade modules was removed as well.

However there is a compromise. you have to call AuthModule.forRoot() on the AppModule (hence the name forRoot()) but you can resolve the configuration and all the initialization work for the auth module when the lazy loaded module is loaded with a Resolver https://angular.io/api/router/Resolve

This would look something like this

/* imports */

@Injectable({
  providedIn: 'root',
})
export class AuthResolver implements Resolve<Promise<any>> {
  constructor(
    @Inject(OidcConfigService)
    private oidcConfigService: OidcConfigService
  ) {}

  resolve(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<any> {
    const action$ = this.oidcConfigService.withConfig({
      /* your config */
    });
    return action$;
  }
}

and then when you load the lazy loaded module you can do

{
    path: 'customers',
    loadChildren: () =>
      import('./customers/customers.module').then((m) => m.CustomersModule),
    resolve: { auth: AuthResolver },
},

where CustomersModule is the lazy loaded module.

Hope this helps

Fabian

Sorry to be negative. Maybe I am misunderstanding it. From reading this, this doesn't seem like a solution that is workable. The whole point of a lazy loaded module or library is that it should be self contained. Every module that calls the auth module has to reference the oauth library to do the resolve.

Hey @stonecourier , thanks for your comment. I agree. We rely on the App initializer to load the configuration currently. A lazy loaded module has no app initializer, see the github issue which I linked in the comment above. So currently it is not possible to have a completely self contained lazy loaded module which contains all of the authentication bits and calling forRoot() in the lazy loaded module with this version. Currently you have to load the AuthModule.forRoot() in the AppModule or a non lazy loaded module which you import eagerly. When the config is being set, you can steer that with the comment I made above. We maybe will target the config and the lazy loaded bits in future versions. But forRoot() is to be called in the Root module. Otherwise you would call forChild() like for example the RouterModule from Angular does. But even a forChild() also needs a forRoot() to be called prior to the forChild() call.

For you this would mean that you can call the AuthModule.forRoot() in the AppModule, and set the config immediatelly. Second you can call the AuthModule.forRoot() in the AppModule and set the config in the lazy loaded module, which I described above. Third you can stick to version 10 for the moment and set the config like before. Like written, we have to see if we solve this in future versions.

I really hope I could help you.

Thanks

Fabian

Was this page helpful?
0 / 5 - 0 ratings