auth0 / auth0-angular

Auth0 SDK for Angular Single Page Applications
MIT License
178 stars 58 forks source link

Unable to set httpInterceptor when providing configuration via AuthClientConfig.set #70

Closed mendhak closed 4 years ago

mendhak commented 4 years ago

Describe the problem

I am using the new dynamic configuration to set the various Auth0 Config properties like domain, clientId, and httpInterceptor.

Problem - The httpInterceptor uri/audience options aren't being picked up. As a result, my http.get API calls do not have an Authorization header on them.

What was the expected behavior?

Authorization header should appear on API requests when specified in httpInterceptor

Reproduction

Following the Dynamic Configuration instructions, I didn't pass anything in the .forRoot() in app.module.ts imports section.

AuthModule.forRoot()

I set up the App Initializer and HTTP Interceptor:

{
      provide: APP_INITIALIZER,
      useFactory: configInitializer,    // <- pass your initializer function here
      deps: [AuthClientConfig,HttpClient],
      multi: true,
},
{ provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true },

I set the values directly in the configInitializer

function configInitializer(config: AuthClientConfig, http: HttpClient) {

  return () => { config.set({ 
        clientId: "myclientid", 
        domain: "mytenant.eu.auth0.com",
        httpInterceptor: { allowedList: [
            {
                uri: "/api/*",
                tokenOptions: {
                    audience: "my-audience"
                }
            }
        ] }
    }); 
  }
}

The httpInterceptor does not seem to get picked up. The library does not pass any audience when requesting a token, and no Authorization header is present on calls to /api/....

If I just copy paste that configuration back into .forRoot(), all behavior returns to normal, and Authorization header is passed in API requests.

CC @stevehobbsdev please can you try this out, does httpInterceptor work for you?

Environment

frederikprijck commented 4 years ago

Hi @mendhak ,

I managed to reproduce it to some degree, however I am seeing an error being logged. Do you as well? I want to ensure we are talking about the same issue and that the fix I provided will also solve your use-case.

This is the error I am seeing:

core.js:4352 ERROR TypeError: Cannot read property 'httpInterceptor' of undefined
    at AuthHttpInterceptor.intercept (auth.interceptor.ts:34)
    at HttpInterceptorHandler.handle (http.js:1258)
    at HttpXsrfInterceptor.intercept (http.js:1886)
    at HttpInterceptorHandler.handle (http.js:1258)
    at HttpInterceptingHandler.handle (http.js:1936)
    at MergeMapSubscriber.project (http.js:1082)
    at MergeMapSubscriber._tryNext (mergeMap.js:46)
    at MergeMapSubscriber._next (mergeMap.js:36)
    at MergeMapSubscriber.next (Subscriber.js:49)
    at Observable._subscribe (subscribeToArray.js:3)

Thanks,

mendhak commented 4 years ago

Hi @frederikprijck thanks for looking.

I get a different error, mine appears when I do an http.get, then in the browser console I see an error appear.

ERROR TypeError: this.config is undefined Angular 6 intercept handle intercept handle handle request/events$< RxJS 12 getWeatherForecast protected.component.ts:25 ProtectedComponent protected.component.ts:17 ProtectedComponent_Factory main.js:211 Angular 11 RxJS 95 core.js:4352

I don't know enough to say what the problem could be.

No error appears elsewhere, are you handling a catch somewhere else? Is there something I could try myself and see if I can match your error?

frederikprijck commented 4 years ago

It sounds related to the error I am seeing, maybe a different error message because of an older Angular? Would u be able to verify if the patch in the PR works in your case?

No error appears elsewhere, are you handling a catch somewhere else? I could try myself and see if I can match your error.

No catch, this line is failing because this,config is undefined (which is also what your error mentions)

mendhak commented 4 years ago

OK I will try your branch. Correct me if I'm wrong (since I am new to this): I need to clone your branch, then ng build it, then reference dist directory in my own package.json and try my http.get again.

frederikprijck commented 4 years ago

It might be a bit more complicated than that to be honest. We should have a release out soon, which should solve it I think. Might be worth trying once we have a release.

One easier way could be: you should be able to create your own interceptor and copy our interceptor"s code and not use ours directly. Be sure to use the patched version: https://github.com/auth0/auth0-angular/blob/fix/interceptor/projects/auth0-angular/src/lib/auth.interceptor.ts

mendhak commented 4 years ago

Hey I tried it slightly differently before I saw your comment. After the npx ng build against your branch, I copied what was in the dist/ into my local project's node_modules/@auth0/ and replaced the auth0-angular directory. That seems simple enough.

I restarted my local Angular application, and your changes worked! 😁

I can see an Authorization Bearer token in the requests.

image

frederikprijck commented 4 years ago

Great, thanks for verifying!

frederikprijck commented 4 years ago

Hi @mendhak ,

This should be fixed in the latest release (1.2.0)!

mendhak commented 4 years ago

Tested 1.2.0 and working, thanks a lot for your efforts with the past few issues @frederikprijck and @stevehobbsdev

I'm really glad we're getting to use this library in our new project; it's easy to use, the instructions are easy to follow, with very good examples. This has made securing our application really easy with minimal effort, with so little cognitive load on our developers, it's an absolute joy to set up for them 😄

My favorite part is the API syntax, minimal and clean.

{
        uri: '/api/accounts/*',
        tokenOptions: {
          audience: 'http://my-api/',
          scope: 'read:accounts',
        },
      },
frederikprijck commented 4 years ago

Thanks for that feedback @mendhak , we are happy you are liking the library and it is enabling you to implement security with minimal effort! 🎉

gagaXD commented 4 years ago

Hey guys,

I've tried to setup dynamic loading of configuration + HTTP_INTERCEPTORS.

Unlike @mendhak , I use httpClient to load my configuration

This is the function that is called in the APP_INITIALIZER method

public loadEnv(): Observable<any> {
    const http = this.injector.get(HttpClient);
    const config = this.injector.get(AuthClientConfig);
    return http.get('/assets/config/environment.json').pipe(
      tap((data: Environnement) => {
        environment.settings = Object.assign({}, data.settings);
        config.set({
          domain: environment.settings.auth0.domain,
          clientId: environment.settings.auth0.clientId,
          httpInterceptor: {
            allowedList: [
              {
                uri: environment.settings.api + '/*',
                tokenOptions: {
                  scope: 'my-scope',
                  audience: environment.settings.auth0.audience,
                },
              },
            ],
          },
        });
      }),
      catchError((err: HttpErrorResponse) => {
        console.warn('No environment.json file found, use default environment.');
        console.error(err);
        config.set({
          domain: environment.settings.auth0.domain,
          clientId: environment.settings.auth0.clientId,
          httpInterceptor: {
            allowedList: [
              {
                uri: environment.settings.api + '/*',
                tokenOptions: {
                  scope: 'my-scope',
                  audience: environment.settings.auth0.audience,
                },
              },
            ],
          },
        });
        return new Observable((subscriber) => {
          subscriber.next();
          subscriber.complete();
        });
      })
    );
  }

My module look like this (I'm loading Auht0Module through my service module, but, I've also tried to load it directly in the appModule, and got the same error)

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    HttpClientModule,
    AuthModule.forRoot(),
  ],
})
export class ServicesModule {
  public static forRoot(): ModuleWithProviders<ServicesModule> {
    return {
      ngModule: ServicesModule,
      providers: [
        ...SERVICE_PROVIDERS,
        {
          provide: APP_INITIALIZER,
          useFactory: init_app,
          deps: [EnvironmentService],
          multi: true,
        },
        { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true },
      ],
    } as ModuleWithProviders<ServicesModule>;
  }
}

At the initialization of my app , I got the following Error

environment.service.ts:37 Error: Configuration must be specified either through AuthModule.forRoot or through AuthClientConfig.set
    at Object.createClient [as useFactory] (auth0-auth0-angular.js:18)
    at Object.factory (core.js:11378)
    at R3Injector.hydrate (core.js:11289)
    at R3Injector.get (core.js:11111)
    at injectInjectorOnly (core.js:899)
    at Module.ɵɵinject (core.js:903)
    at Object.AuthHttpInterceptor_Factory [as factory] (auth0-auth0-angular.js:491)
    at R3Injector.hydrate (core.js:11289)
    at R3Injector.get (core.js:11111)
    at injectInjectorOnly (core.js:899)

If I remove the HTTP_INTERCEPTORS, the application load, but, obviously, Access_token is not attached to the requets

It seems that Angular is trying to init the Interceptors for the HttpClient, but, since I've not supplied a configuration yet, the Auth0Interceptor fails to init.

Am I missing something in order to make this works ?

Sorry for commenting in a closed issue, but it seems really related to this issue !

mendhak commented 4 years ago

Hi @gagaXD I am using a web request to fetch my configuration, I just didn't show it in my example above to simplify things. Let me share what I have, and you can try and compare and see what's different or get a clue.

In app.module.ts, under providers I have:

providers: [
    AppConfigService,
    { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true },
    { provide: APP_INITIALIZER,useFactory: initializeApp, deps: [AppConfigService], multi: true}
  ],

The initializeApp method is:

import { AppConfigService } from './app-config.service';

export function initializeApp(appConfigService: AppConfigService) {
  return (): Promise<any> => { 
    return appConfigService.load();
  }
}

You can see it's actually calling another service.

Now in my app-config.service.ts note that I'm using HttpBackend with the HttpClient so that the interceptor doesn't intercept :laughing: The reason for the separate class is because it's going to hold all my other UI settings, I want to pass it around my application.

import { Injectable }  from '@angular/core';
import { HttpClient, HttpBackend } from '@angular/common/http';
import { AuthClientConfig, AuthConfig, AuthConfigService } from '@auth0/auth0-angular';

@Injectable()
export class AppConfigService {
    static settings: IAppConfig;
    httpClient: HttpClient;
    handler: HttpBackend;
    authClientConfig: AuthClientConfig;

    constructor(private http: HttpClient, handler: HttpBackend, authClientConfig: AuthClientConfig) {
        this.httpClient = http;
        this.handler = handler;
        this.authClientConfig = authClientConfig;
    }

    load() {

        const jsonFile = `https://api.npoint.io/86e813b730424ff5fcd0`;
        return new Promise<void>((resolve, reject) => {
            this.httpClient = new HttpClient(this.handler);
            this.httpClient.get(jsonFile).toPromise().then((response : IAppConfig) => {
               AppConfigService.settings = <IAppConfig>response;

               this.authClientConfig.set({ 
                clientId: AppConfigService.settings.auth0ClientId, domain: AppConfigService.settings.auth0Domain,
                httpInterceptor: { allowedList: [
                    {
                        uri: "/api/*",
                        tokenOptions: {
                            audience: "my-api"
                        }
                    }
                ] }
             });

               console.log('Config Loaded');
               console.log( AppConfigService.settings);
               resolve();

            /*}).catch((response: any) => {
               reject(`Could not load the config file`);*/
            });
        });
    }
}

export interface IAppConfig {

    auth0ClientId: string
    auth0Domain: string
}
gagaXD commented 4 years ago

Thx @mendhak

The important line I was missing was in my loading service

this.httpClient = new HttpClient(this.handler);

With this line, you create a new HttpClient, that "ignore" HTTP_INTERCEPTOR. Seems more like a workaround to me, but, it works great, so I'll go with this solution !

Thank you for the help !

stevehobbsdev commented 4 years ago

Thanks for the example @mendhak - glad you were able to help @gagaXD with it 👍

mendhak commented 4 years ago

No worries, I was doing my own writeup on Angular + auth0-angular library + dynamic configuration. Hope it helps future searchers since there are a lot of moving parts to getting it set up properly.

dsebastien commented 3 years ago

I'm also facing this issue:

main.ts:34 Error: Configuration must be specified either through AuthModule.forRoot or through AuthClientConfig.set
    at Object.createClient [as useFactory] (auth0-auth0-angular.js:18)
    at Object.factory (core.js:11247)
    at R3Injector.hydrate (core.js:11158)
    at R3Injector.get (core.js:10979)
    at injectInjectorOnly (core.js:4907)

Unfortunately I have other app initializers that ultimately need to access the HttpClient. I'm using the this.httpClient = new HttpClient(handler); trick in my config service, but can't use it in the other initializers because those need to have the access token attached.

I've tried ordering the app initializers using this https://github.com/fvilers/ngx-ordered-initializer but it didn't help. I still end up with the same error.

frederikprijck commented 3 years ago

I think that's a hard thing to solve @dsebastien . If u have App Initializers that need the access token but only get the details for initializing the Auth0 SDK in another App Initializer, it can get tricky.

U could fetch the config outside of Angular and only once that call is done bootstrap the Angular App and use the config directly, that should make it available in AppInitializers.

dsebastien commented 3 years ago

Ok ok, I see what you mean. Indeed, that should do it. I just have to jump through some hoops to pass the config loaded outside of angular to the config service; I guess that I can put it in a global or in local storage for a second ;-)

I would've hoped to be able to set a temporary config with AuthModule.forRoot, and update it a jiffy afterwards. But indeed, it's probably complicated on your end.

Thanks @frederikprijck

frederikprijck commented 3 years ago

Ye I think this is partly because of how Angular handles AppInitializers and their order.

I would've hoped to be able to set a temporary config with AuthModule.forRoot, and update it a jiffy afterwards. But indeed, it's probably complicated on your end.

U can do that... But it is not going to solve your problem as your AppInitializers will use the static config passed to forRoot while they might need the dynamic config that is being loaded in the other App Initializer.

dsebastien commented 3 years ago

What I mean is that the Auth0 interceptor could probably be notified when the AuthConfig config changes, and use the new one instead of having it statically defined once and for all. Anyways, I won't bother you anymore, I think I can move on with the project ;-)

Thanks again