Samuel
Samuel

Reputation: 1416

Passing Bearer token to a different APP_INITIALIZER to load config from server in Angular App

I went through several questions like 1 , 2 but I don't know how to make my app work.

Problem: When I sign in 1st time, I do not get Bearer token and hence my SettingConfigService fails with 401, if I refresh the page, I get the token from this.oauth.getAccessToken() because now the token is in localstorage.

I am using oauth lib for login. Here is the modules and libs I have created.

App.module


export function AppConfigurationFactory(config: SettingConfigService) {
  return async () => await config.ensureInit(APP_NAME);
}

export class AppConfig {
  baseUrl: string;
  production: boolean;
}

export const appConfig: AppConfig = {
  baseUrl: environment.baseUrl,
  production: environment.production,
};

@NgModule({
  exports: [],
  declarations: [

  ],
  imports: [
     ....
    CustomAuthModule.forRoot(environment.keycloak),
    CustomInfrastructureModule.forRoot({ appConfig }),
    SharedModule,
  ],
  providers: [
    { provide: AppConfig, useValue: appConfig }, 
    ...
    {
      provide: APP_INITIALIZER,
      useFactory: AppConfigurationFactory,
      deps: [ SettingConfigService, HttpClient, TranslateService, OAuthService],
      multi: true,
    },
  ],
})
export class AppModule {}

CustomAuthModule.ts

import { NgModule, APP_INITIALIZER, Optional, SkipSelf, ModuleWithProviders, InjectionToken } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { OAuthModule, AuthConfig } from 'angular-oauth2-oidc';
import { OAuthModuleConfig,CustomAuthConfigParams } from './auth.config';
import { AuthConfigService } from './auth.service';

export function init_app(authConfigService: AuthConfigService) {
  return () => authConfigService.initAuth();
}

@NgModule({
  imports: [HttpClientModule, OAuthModule.forRoot()]
})
export classCustomAuthModule {
  constructor(@Optional() @SkipSelf() parentModule:CustomAuthModule){
    if(parentModule){
        throw new Error('QontrolAuthModule is already loaded.');
    }
  }

  static forRoot(keycloakParams): ModuleWithProviders<QontrolAuthModule> {
    return {
      ngModule:CustomAuthModule,      
      providers: [ 
        AuthConfigService,
        OAuthModuleConfig,
        { 
          provide: AuthConfig, 
          useValue: keycloakParams
        },
        {
          provide: APP_INITIALIZER,
          useFactory: init_app,
          deps: [AuthConfigService],
          multi: true,
        }, ]
    }
  }
}

CustomInfrastrucutureModule.ts

@NgModule({
  declarations: [],
  imports: [CommonModule],
  exports: [],
  providers: [],
})
export class CustomInfrastructureModule {

  static forRoot(conf?: {
    appConfig: SharedInfrastructureAppConfig;
  }): ModuleWithProviders<CustomInfrastructureModule> {
    return {
      ngModule: CustomInfrastructureModule,
      providers: [
        { provide: APP_CONFIG, useValue: conf.appConfig },
        {
          provide: LOCALE_ID,
          deps: [SettingConfigService], // some service handling global settings
          useFactory: (config: SettingConfigService) => config.culture
        },
      ],
    };
  }
}

SettingConfigService


@Injectable({providedIn: 'root'})
export class SettingConfigService {
  culture: string;
  config: any;

  constructor(
    private httpClient: HttpClient,
    @Inject(APP_CONFIG) protected appConfig: SharedInfrastructureAppConfig,
    private oauth: OAuthService
  ) { }

  async ensureInit(clientPrefix: string): Promise<void>{
    console.log(this.oauth.getAccessToken());  //<-- comes as null when 1st login    
   // putting a wait time of 1 sec as per https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep makes it work,
     // because by that time, we have the token in localStorage
    const response = await this.httpClient.get<any>(`${this.appConfig.baseUrl}/configs`).toPromise();
    this.config = response;
  }
}

Here is my code which fetched the token using oauth2-oidc which is called from

AuthConfigService

  async initAuth(): Promise<any> {
    return new Promise((resolveFn, rejectFn) => {
      this.oauthService.configure(this.authConfig);
      // Redirect to path, if there is one
      if (window && window.location && window.location.pathname) {
        this.oauthService.redirectUri = window.location.origin + window.location.pathname;
      }

      this.oauthService.setStorage(localStorage);
      this.oauthService.tokenValidationHandler = new NullValidationHandler();

      this.oauthService.events
        .pipe(
          filter((e: any) => {
            return e.type === 'token_received';
          })
        )
        .subscribe(() => 
           this.handleNewToken() // <-- this takes time to get triggered and meanwhile
                       // the call to SettingConfigService is made
        );

      this.oauthService.loadDiscoveryDocumentAndLogin().then((isLoggedIn) => {
        if (isLoggedIn) {
          this.oauthService.setupAutomaticSilentRefresh();
          resolveFn(() => {});
        } else {
          console.log(this.oauthService.getAccessToken())
          this.oauthService.initImplicitFlow();
          console.log(this.oauthService.getAccessToken())
          rejectFn();
        }
      });
    });
  }

In short,

I need to synchronize APP_INITIALIZER of app.module.ts to wait for token of APP_INITIALIZER of CustomAuthModule, and then it'll have a bearer token (which gets added by interceptor). Is my understanding correct? Please help

Upvotes: 3

Views: 1140

Answers (1)

Shashank Vivek
Shashank Vivek

Reputation: 17504

You need to follow the proper sequence to load config with the token. try:

app.module

@NgModule({
  exports: [],
  declarations: [

  ],
  imports: [
     ....
    CustomAuthModule.forRoot(environment.keycloak, clientPrefixConstant),
    CustomInfrastructureModule.forRoot({ appConfig }),
    SharedModule,
  ],
  providers: [
    { provide: AppConfig, useValue: appConfig }, 
    ...
  ],
})
export class AppModule {}

in AuthModule


export const CLIENT_NAME = new InjectionToken<string>('CLIENT_PARAM');

export function init_app(authConfigService: AuthConfigService,
    settingSvc: SettingConfigService,
    clientPrefix: string 
   ) {
  return () => authConfigService.initAuth().then(async () => {
      await settingSvc.ensureInit(clientName);
    });
}

@NgModule({
  imports: [HttpClientModule, OAuthModule.forRoot()]
})
export classCustomAuthModule {
  ....

  static forRoot(keycloakParams, clientPrefix): ModuleWithProviders<QontrolAuthModule> {
    return {
      ngModule:CustomAuthModule,      
      providers: [ 
        AuthConfigService,
        OAuthModuleConfig,
        { 
          provide: AuthConfig, 
          useValue: keycloakParams
        },
        {
          provide: CLIENT_NAME,
          useValue: clientPrefix
        },
        {
          provide: APP_INITIALIZER,
          useFactory: init_app,
          deps: [AuthConfigService,SettingConfigService,CLIENT_NAME],
          multi: true,
        }, ]
    }
  }
}

What was happening in your code is that both the APP_INITIALIZER are called in parallel, which resulted in unavailability of token. With my suggested answer, you resolve the token 1st and then you call ensureInit. With InjectionToken,I accepted the string value which was required to call the function.

Upvotes: 2

Related Questions