angularauthorizationkeycloakangular17keycloak-angular

Configure Angular 17 standalone to work with Keycloak


Issue

I'm working with an Angular v17 app configured in standalone mode, experiencing issues integrating with Keycloak libraries. Specifically, Keycloak isn't automatically appending the authorization header to backend requests. For security reasons, I prefer not to manually handle the Authorization Token.

All this code is working well with Angular non standalone (NgModule). But since I switched to standalone in angular 17, something is fishy.

To test my code, I have configured an Interceptor: authInterceptorProvider. That is adding the Token manually to each request. Works well. But I don't want to handle tokens by hand...

What might I be missing or configuring wrong?

Code bits (image upload is not working at the moment)

Here my simplyfied Application config

  export const initializeKeycloak = (keycloak: KeycloakService) => {
return () =>
  keycloak.init({
    config: {
      url: 'http://localhost:8180/',
      realm: 'balbliblub-realm',
      clientId: 'blabliblubi-public-client',
    },
    initOptions: {
      pkceMethod: 'S256',
      redirectUri: 'http://localhost:4200/dashboard',
    },
    loadUserProfileAtStartUp: false
  });}


export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes),
  provideHttpClient(
    withFetch(),
    withXsrfConfiguration(
    {
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN',
    })
  ),

  authInterceptorProvider,
  importProvidersFrom(HttpClientModule, KeycloakBearerInterceptor),
  {
    provide: APP_INITIALIZER,
    useFactory: initializeKeycloak,
    multi: true,
    deps: [KeycloakService],
  },
  KeycloakService,
]};

Here my AppComponent

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent implements OnInit {
  title = 'testy';
  public isLoggedIn = false;
  public userProfile: KeycloakProfile | null = null;

  constructor(private readonly keycloak: KeycloakService,
              private http: HttpClient) { }

  public async ngOnInit() {
    this.isLoggedIn = await this.keycloak.isLoggedIn();

    if (this.isLoggedIn) {
      this.userProfile = await this.keycloak.loadUserProfile();
    }
  }

  login() {
    this.keycloak.login();
  }

  protected loadAbos() {
    this.http.get<Abo[]>('http://localhost:8080/api/abos?email=' + this.userProfile?.email, { observe: 'response',withCredentials: true })
      .pipe(
        catchError(err => this.handleError("Could not load abos", err)),
        /// if no error occurs we receive the abos
        tap(abos => {
          console.info("loaded abos", abos);
        })
      ).subscribe()
  }

Thanks 4 your help <3


Solution

  • Here is a response featuring a working example, including Angular 17 standalone and Keycloak 23 https://github.com/mauriciovigolo/keycloak-angular/issues/384#issuecomment-1895845160

    and here are full app.config.ts

    import { ApplicationConfig } from '@angular/core';
    import { provideRouter, withComponentInputBinding, withInMemoryScrolling, withViewTransitions } from '@angular/router';
    
    import { routes } from './app.routes';
    import { provideClientHydration } from '@angular/platform-browser';
    import { provideAnimations } from '@angular/platform-browser/animations';
    import { DOCUMENT } from '@angular/common';
    import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
    import { HttpClientModule, HttpClient } from '@angular/common/http';
    import { TranslateHttpLoader } from '@ngx-translate/http-loader';
    import {
      provideHttpClient,
      withFetch,
    } from '@angular/common/http';
    import {KeycloakBearerInterceptor, KeycloakService} from "keycloak-angular";
    import {HTTP_INTERCEPTORS, withInterceptorsFromDi} from "@angular/common/http";
    import {APP_INITIALIZER, Provider} from '@angular/core';
    
    
    export function HttpLoaderFactory(http: HttpClient) {
      return new TranslateHttpLoader(http,'./assets/i18n/', '.json');
    }
    function initializeKeycloak(keycloak: KeycloakService) {
      return () =>
        keycloak.init({
          // Configuration details for Keycloak
          config: {
            url: 'http://localhost:8082', // URL of the Keycloak server
            realm: 'realm-name', // Realm to be used in Keycloak
            clientId: 'clientid' // Client ID for the application in Keycloak
          },
          // Options for Keycloak initialization
          initOptions: {
            onLoad: 'login-required', // Action to take on load
            silentCheckSsoRedirectUri:
              window.location.origin + '/assets/silent-check-sso.html' // URI for silent SSO checks
          },
          // Enables Bearer interceptor
          enableBearerInterceptor: true,
          // Prefix for the Bearer token
          bearerPrefix: 'Bearer',
          // URLs excluded from Bearer token addition (empty by default)
          //bearerExcludedUrls: []
        });
     }
     
     // Provider for Keycloak Bearer Interceptor
     const KeycloakBearerInterceptorProvider: Provider = {
      provide: HTTP_INTERCEPTORS,
      useClass: KeycloakBearerInterceptor,
      multi: true
     };
     
     // Provider for Keycloak Initialization
     const KeycloakInitializerProvider: Provider = {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService]
     }
     
     
    export const appConfig: ApplicationConfig = {
      providers: [
        provideHttpClient(withInterceptorsFromDi()), // Provides HttpClient with interceptors
        KeycloakInitializerProvider, // Initializes Keycloak
        KeycloakBearerInterceptorProvider, // Provides Keycloak Bearer Interceptor
        KeycloakService, // Service for Keycloak 
        provideRouter(routes,withViewTransitions(),withComponentInputBinding()), 
        provideClientHydration(), 
        // provideHttpClient(withFetch()),
        provideAnimations(), 
        { provide: Document, useExisting: DOCUMENT },
        TranslateModule.forRoot({
          defaultLanguage: 'ar',
          loader: {
            provide: TranslateLoader,
            useFactory: HttpLoaderFactory,
            deps: [HttpClient]
          }
        }).providers!
      ]
    };
    
    

    also I have used it in my components like

    import { KeycloakService } from 'keycloak-angular';
    @Component({
      selector: 'app-customers',
      standalone: true,
      imports: [CommonModule, MatIconModule, MatPaginatorModule, MatCardModule, MatToolbarModule, MatButtonModule, CustomeSearchComponent],
    
      templateUrl: './customers.component.html',
      styleUrl: './customers.component.scss'
    })
    export class CustomersComponent implements OnInit {
      constructor( private keycloakService: KeycloakService) {
    }
    
      ngOnInit(): void {
        const isLoggedIn = this.keycloakService.isLoggedIn();
        if (!isLoggedIn)
          this.keycloakService.login();
    
        const userRoles = this.keycloakService.getUserRoles();
     
    
        if (isLoggedIn){
           this.loadData();
        }      
      }
    
    }
    
    }
    

    This will automatically include authentication header to your http calls