Search code examples
angularkeycloakangular-elementsangular-standalone-componentskeycloak-angular

keycloak-angular with AngularElements and Standalone Component (no Authorization Header added to requests)


I have an angular app (and a nestjs backend) and I try to handle user login with the help of keycloak. As the buzzword rich question title states I have kind of a "weird" architecture.

  • I want to create a custom element with the help of angular (works)
  • I thought I don't need an own module for the component I build, so it is a standalone component (works)
  • I use keycloak-angular in main.ts to do the login (works, I get a token, username, roles,...)
  • I use the HttpClient for the requests of that component (custom element) to its backend (requests do work, but the bearer token is not added)

According to the readme of keycloak-angular:

"By default, all HttpClient requests will add the Authorization header"

Also when I add a shouldAddToken function to the KeycloakOptions that I set in the keycloakService.init method, this function is never called.

I thought, maybe the HttpClient in the component is another instance than the one in main.ts but I checked it and it is the same instance. (have set (httpService as any).mytest = 1; in main.ts and the httpService in the component had that parameter).

Any idea what I could miss?

The relevant code in main.ts

(async () => {

    // inspired by https://www.angulararchitects.io/aktuelles/angular-elements-web-components-with-standalone-components/
    const app = await createApplication({
        providers: [
            provideHttpClient(),
            KeycloakService,
            {
                // I guess this should not be necessary, but I tryed anyway
                provide: HTTP_INTERCEPTORS,
                useClass: KeycloakBearerInterceptor,
                multi: true,
                deps: [KeycloakService]
            },
        ],
    });


    try {
        await initializeKeycloak(app);
    } catch (err: any) {
        console.error('Seems like there was a problem with the login', err);
        return;
    }

    // register my root component as CustomElement
    customElements.define(
        'my-component',
        createCustomElement(AppComponent, { injector: app.injector })
    );

})();    
async function initializeKeycloak(app: ApplicationRef) {
    
    const keycloakService = app.injector.get(KeycloakService);
                
    return keycloakService.init({
        
        // keycloak address and client data coming from environment file
        config: environment.keycloak.config,

        /* without this getUsername() returns the Error "User not logged in or user profile was not loaded." */
        loadUserProfileAtStartUp: true,

        initOptions: {
            enableLogging: true,
            onLoad: 'login-required',
            // someone on the web had the same problem (no auth header) and fixed it with this (not sure if this is relevant, it does not help in my case)
            silentCheckSsoRedirectUri:
                window.location.origin + '/assets/silent-check-sso.html',
        },
        shouldAddToken: (request) => {
            // is never logged
            console.log('XXXXXXXXXXX addToken?', request);
            return true;
        }
        
    }).then(async loggedIn =>  {
        if (loggedIn) {
            
            // returns sane data
            console.log("loggedIn: ", {
                loggedIn,
                username: keycloakService.getUsername(),
                roles: keycloakService.getUserRoles().join(', '),
                jwt: jwt_decode(await keycloakService.getToken())
            });
            
            // returns true
            console.log("keycloakService.enableBearerInterceptor", keycloakService.enableBearerInterceptor);

        } else {
            console.log(`keycloak.init returned ` + loggedIn);
        }
        
        return loggedIn;
    });
}

Update

Two things I am not sure about and that could cause the Keycloak Interceptor to not add the bearer token:

Maybe they are not added if the domain is different?

  • my keycloak is at http://test.local:8080
  • my angular app at http://localhost:4200
  • my backend app at http://localhost:3333

Maybe the HttpInterceptor needs to be added for every module that does http request?! Already tried to add the following (without success) to one of the modules that is imported by my AppModule and does requests via the HttpClient.

{
    provide: HTTP_INTERCEPTORS,
    useClass: KeycloakBearerInterceptor,  
    multi: true, 
    ...

The HttpClient is the same instance everywhere (as written earlier).


Solution

  • Problem solved.

    As I use a standalone component I can't add the HttpClientModule to the imports array of the AppModule. The new way of doing this is the method provideHttpClient.

    Instead of adding constructs like this to your providers array

    {
       // I guess this should not be necessary, but I tryed anyway
       provide: HTTP_INTERCEPTORS,
       useClass: KeycloakBearerInterceptor,
       multi: true,
       deps: [KeycloakService]
    },
    

    you can add the new "functional interceptors" to the provideHttpClient like so

    provideHttpClient(
        withInterceptors([ requestInterceptor ]) 
    )
    

    where requestInterceptor is a simple function instead of a service

    function requestInterceptor(
        req: HttpRequest<unknown>,
        next: HttpHandlerFn
    ): Observable<HttpEvent<unknown>> {
        ...
    }
    

    You can also add "Legacy Interceptors" like before BUT you have to tell the new API to search and inject them in your providers via withInterceptorsFromDi(). And that's what was missing in my code.

    Looks like this now

    const app = await createApplication({
        providers: [
            ...,
            provideHttpClient(
                withInterceptors([
                    // just testing the new way of intercepting
                    requestInterceptor
                ]),
                withInterceptorsFromDi()
            ),
            KeycloakAngularModule,
            KeycloakService,
            KeycloakBearerInterceptor,
            {
                provide: HTTP_INTERCEPTORS,
                useClass: KeycloakBearerInterceptor,
                multi: true,
                deps: [KeycloakService]
            },
            {
                // testing own legacy interceptor
                provide: HTTP_INTERCEPTORS,
                useClass: LoggingInterceptor,
                multi: true
            },
        ],
    });
    

    All 3 interceptors work.

    This article was my knight in shining armor