Search code examples
angularazure-active-directoryconfiguration-files

"Cannot read property 'displayCall' of undefined at new AuthenticationContex ..."


How is it going?

So, I just started working with the newest version of Angular using TypeScript and I'm having a problem while trying to load a configuration file into my app.

Things go pretty much like this: I'm working on a project that uses MsAdalAngular6Module for Azure authentication. When I got the project all configuration settings were hard coded, so I followed the instructions from: https://devblogs.microsoft.com/premier-developer/angular-how-to-microsoft-adal-for-angular-6-with-configurable-settings/ to use the package in a more realistic scenario. But then, as you gonna find in the first link, I had to create a service to load my configuration files dynamically following those instructions: https://devblogs.microsoft.com/premier-developer/angular-how-to-editable-config-files/

Things went pretty well after this, I was able to load my configuration files, no problem. But the problem started when I needed to create an HttpInterceptor to insert an authentication token in the header of every request. Basically, I can't use both at the same time (the service that loads the configuration file and the HttpInterceptor), or else I get this error:

Error when trying to load configuration files

My guess is: the request coming from the config service is made before the MsAdalAngular6Module loads. So, when the request is made, it does not have some properties that it should have ir order for it to be authenticated by the MsAdalAngular6Service. Of course, it's just a guess.

Here is some code:

Config Service


    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { environment } from '../environments/environment';
    import { IAppConfig } from './models/app-config.model';

    @Injectable()
    export class AppConfig {
        static settings: IAppConfig;
        constructor(private http: HttpClient) { }
        load() {
            const jsonFile = `assets/config/config.${environment.name}.json`;
            return new Promise<void>((resolve, reject) => {
                this.http.get(jsonFile, { withCredentials: true }).toPromise().then((response: IAppConfig) => {
                    AppConfig.settings = <IAppConfig>response;
                    resolve();
                }).catch((response: any) => {
                    reject(`Could not load file '${jsonFile}': ${JSON.stringify(response)}`);
                });
            });
        }
    }

The Http Interceptor


    import { Injectable } from '@angular/core';
    import { HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
    import { mergeMap } from 'rxjs/operators';
    import { MsAdalAngular6Service } from 'microsoft-adal-angular6';

    @Injectable()
    export class InsertAuthTokenInterceptor implements HttpInterceptor {

        constructor(private adal: MsAdalAngular6Service) { }

        intercept(req: HttpRequest<any>, next: HttpHandler) {
            // get api url from adal config
            const resource = this.adal.GetResourceForEndpoint(req.url);
            if (req.withCredentials){
                return next.handle(req);
            }

            if (!resource || !this.adal.isAuthenticated) {
                return next.handle(req);
            }

            // merge the bearer token into the existing headers
            return this.adal.acquireToken(resource).pipe(
                mergeMap((token: string) => {
                    const authorizedRequest = req.clone({
                        headers: req.headers.set('Authorization', `Bearer ${token}`),
                    });
                    return next.handle(authorizedRequest);
            }));
        }
    }

And the way I use it on app.module.ts:

    ...
    let adalConfig: any;

    export const interceptorProviders =
      [
        {provide: HTTP_INTERCEPTORS, useClass: InsertAuthTokenInterceptor, multi: true}
      ];



    export function msAdalAngular6ConfigFactory() {
      return adalConfig;
    }

    export function initializeApp(appConfig: AppConfig) {
      const promise = appConfig.load().then(() => {
        adalConfig = {
          tenant: AppConfig.settings.adalConfig.tenant,
          clientId: AppConfig.settings.adalConfig.clientId,
          endpoints: AppConfig.settings.adalConfig.endpoints,
          navigateToLoginRequestUrl: false,
          cacheLocation: AppConfig.settings.adalConfig.cacheLocation
        };
      });

      return () => promise;
    }

    ...

Providers part

    ...
      providers: [
        AppConfig,
        {
          provide: APP_INITIALIZER,
          useFactory: initializeApp,
          deps: [AppConfig],
          multi: true,
        },
        interceptorProviders,
        AuthenticationGuard,
        LoaderService,
        MsAdalAngular6Service,
        {
          provide: 'adalConfig',
          useFactory: msAdalAngular6ConfigFactory,
          deps: []
        },
        AuthenticationGuard
      ]
    ...

Solution

  • Ok, I guess I found a solution. I just don't know how reliable it's is. I used a local htpp client. Then, the AppConfig service became something like this:

        import { Injectable } from '@angular/core';
        import { HttpBackend, HttpClient } from '@angular/common/http';
        import { environment } from '../environments/environment';
        import { IAppConfig } from './models/app-config.model';
    
        const URL = `assets/config/config.${environment.name}.json`;
    
        @Injectable({ providedIn: 'root' }) export class AppConfig {
            static settings: IAppConfig;
    
            private httpClient: HttpClient; constructor(httpBackend: HttpBackend) {
                // Use local HttpClient to avoid interceptor loop
                this.httpClient = new HttpClient(httpBackend);
            }
    
            getAppConfig() {
                return new Promise<void>((resolve, reject) => {
                    this.httpClient.get(URL, { withCredentials: true }).toPromise().then((response: IAppConfig) => {
                        AppConfig.settings = <IAppConfig>response;
                        resolve();
                    }).catch((response: any) => {
                        reject(`Could not load file '${URL}': ${JSON.stringify(response)}`);
                    });
                });
            }
        }