blog.backToBlog

Professional Keycloak Integration for Angular 19 with SSR

Khaled AMIRAT

Khaled AMIRAT

Founder of Qodefy and Creator of the Qodefy Platforms

April 20, 2025

Professional-Keycloak-Integration-for-Angular-19-with-SSR

Here’s a complete, professional solution for integrating Keycloak authentication and authorization in your Angular 19 application with Server-Side Rendering (SSR) support.

1. Installation

First, install the required packages:

bash

Copy

Download

npm install keycloak-angular keycloak-js
npm install @angular/material @angular/cdk @angular/flex-layout  # Optional for UI components

2. Keycloak Configuration

Create a configuration file for Keycloak:

src/app/config/keycloak.config.ts

typescript

Copy

Download

import { KeycloakConfig } from 'keycloak-js';

const keycloakConfig: KeycloakConfig = {
  url: 'http://your-keycloak-server/auth',
  realm: 'your-realm',
  clientId: 'your-client-id'
};

export const environment = {
  production: false,
  keycloak: keycloakConfig,
  apiUrl: 'http://your-api-url'
};

export const environmentProd = {
  production: true,
  keycloak: {
    ...keycloakConfig,
    url: 'https://your-production-keycloak-server/auth'
  },
  apiUrl: 'https://your-production-api-url'
};

3. Keycloak Initialization Service

Create a service to handle Keycloak initialization:

src/app/core/auth/keycloak-init.service.ts

typescript

Copy

Download

import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { environment } from '../../../config/keycloak.config';

@Injectable({
  providedIn: 'root'
})
export class KeycloakInitService {
  constructor(private keycloak: KeycloakService) {}

  async initialize(): Promise<boolean> {
    try {
      const authenticated = await this.keycloak.init({
        config: environment.keycloak,
        initOptions: {
          onLoad: 'check-sso',
          silentCheckSsoRedirectUri: 
            window.location.origin + '/assets/silent-check-sso.html',
          checkLoginIframe: false
        },
        bearerExcludedUrls: ['/assets', '/public']
      });

      if (authenticated) {
        console.log('User is authenticated');
        // You can load user profile here if needed
      }

      return authenticated;
    } catch (error) {
      console.error('Keycloak initialization failed', error);
      return false;
    }
  }
}

4. Auth Service

Create a comprehensive auth service:

src/app/core/auth/auth.service.ts

typescript

Copy

Download

import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakProfile, KeycloakTokenParsed } from 'keycloak-js';
import { BehaviorSubject, from, Observable } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private userProfile: KeycloakProfile | null = null;
  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  public isAuthenticated$ = this.isAuthenticatedSubject.asObservable();

  constructor(private keycloak: KeycloakService) {
    this.initialize();
  }

  private initialize(): void {
    this.keycloak.isLoggedIn().then(isLoggedIn => {
      this.isAuthenticatedSubject.next(isLoggedIn);
    });
  }

  public getToken(): Promise<string> {
    return this.keycloak.getToken();
  }

  public getParsedToken(): KeycloakTokenParsed | undefined {
    return this.keycloak.getKeycloakInstance().tokenParsed;
  }

  public loadUserProfile(): Observable<KeycloakProfile> {
    return from(this.keycloak.loadUserProfile()).pipe(
      tap(profile => {
        this.userProfile = profile;
      })
    );
  }

  public login(): void {
    this.keycloak.login();
  }

  public register(): void {
    this.keycloak.register();
  }

  public logout(): void {
    this.keycloak.logout(window.location.origin);
  }

  public getRoles(): string[] {
    return this.keycloak.getUserRoles();
  }

  public hasRole(role: string): boolean {
    return this.keycloak.getUserRoles().includes(role);
  }

  public updateToken(minValidity = 30): Observable<boolean> {
    return from(this.keycloak.updateToken(minValidity)).pipe(
      switchMap(refreshed => {
        if (refreshed) {
          console.log('Token was successfully refreshed');
        }
        return this.isAuthenticated$;
      })
    );
  }

  public getUsername(): string | undefined {
    const token = this.getParsedToken();
    return token?.preferred_username || token?.email || token?.sub;
  }
}

5. Auth Guard

Create guards for protected routes:

src/app/core/guards/auth.guard.ts

typescript

Copy

Download

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private router: Router,
    private keycloak: KeycloakService
  ) {}

  async canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Promise<boolean> {
    try {
      const isAuthenticated = await this.keycloak.isLoggedIn();

      if (!isAuthenticated) {
        await this.keycloak.login({
          redirectUri: window.location.origin + state.url
        });
        return false;
      }

      const requiredRoles = route.data['roles'] as Array<string>;
      if (!requiredRoles || requiredRoles.length === 0) {
        return true;
      }

      const hasRole = requiredRoles.some(role => this.keycloak.getUserRoles().includes(role));
      if (!hasRole) {
        this.router.navigate(['/access-denied']);
        return false;
      }

      return true;
    } catch (error) {
      console.error('AuthGuard error', error);
      this.router.navigate(['/']);
      return false;
    }
  }
}

6. HTTP Interceptor

Create an interceptor to add the token to requests:

src/app/core/interceptors/auth.interceptor.ts

typescript

Copy

Download

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable, from, switchMap } from 'rxjs';
import { KeycloakService } from 'keycloak-angular';
import { environment } from '../../config/keycloak.config';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private keycloak: KeycloakService) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    // Skip if the request is to keycloak or excluded URLs
    if (
      !this.keycloak.isLoggedIn() ||
      request.url.includes(environment.keycloak.url) ||
      environment.keycloak.bearerExcludedUrls.some(url => request.url.includes(url))
    ) {
      return next.handle(request);
    }

    return from(this.keycloak.getToken()).pipe(
      switchMap(token => {
        if (token) {
          request = request.clone({
            setHeaders: {
              Authorization: `Bearer ${token}`
            }
          });
        }
        return next.handle(request);
      })
    );
  }
}

7. Silent Check SSO HTML

Create a silent check SSO file:

src/assets/silent-check-sso.html

html

Copy

Download

Run

<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>

8. App Module Configuration

Configure the AppModule:

src/app/app.module.ts

typescript

Copy

Download

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AuthInterceptor } from './core/interceptors/auth.interceptor';
import { KeycloakInitService } from './core/auth/keycloak-init.service';
import { environment } from '../config/keycloak.config';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    HttpClientModule,
    KeycloakAngularModule,
    AppRoutingModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    },
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakInitService]
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

export function initializeKeycloak(keycloakInitService: KeycloakInitService) {
  return () => keycloakInitService.initialize();
}

9. Server Module Configuration (for SSR)

src/app/app.server.module.ts

typescript

Copy

Download

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { KeycloakService } from 'keycloak-angular';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule
  ],
  providers: [
    // Add universal-only providers here
    {
      provide: KeycloakService,
      useValue: null // Disable Keycloak on server side
    }
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

10. Navbar Component with Login/Register Buttons

src/app/layout/navbar/navbar.component.ts

typescript

Copy

Download

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../../core/auth/auth.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.scss']
})
export class NavbarComponent implements OnInit {
  isAuthenticated$: Observable<boolean>;
  username$: Observable<string | undefined>;

  constructor(private authService: AuthService) {
    this.isAuthenticated$ = this.authService.isAuthenticated$;
    this.username$ = this.isAuthenticated$.pipe(
      map(isAuthenticated => isAuthenticated ? this.authService.getUsername() : undefined)
    );
  }

  ngOnInit(): void {
    this.authService.updateToken().subscribe();
  }

  login(): void {
    this.authService.login();
  }

  register(): void {
    this.authService.register();
  }

  logout(): void {
    this.authService.logout();
  }
}

src/app/layout/navbar/navbar.component.html

html

Copy

Download

Run

<nav>
  <!-- Your navbar content -->
  
  <div *ngIf="!(isAuthenticated$ | async); else authenticatedUser">
    <button (click)="login()">Login</button>
    <button (click)="register()">Register</button>
  </div>

  <ng-template #authenticatedUser>
    <div>
      <span>Welcome, {{ username$ | async }}</span>
      <button (click)="logout()">Logout</button>
    </div>
  </ng-template>
</nav>

11. Route Configuration

src/app/app-routing.module.ts

typescript

Copy

Download

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
import { HomeComponent } from './pages/home/home.component';
import { BlogComponent } from './pages/blog/blog.component';
import { WeatherComponent } from './pages/weather/weather.component';
import { IaConversationComponent } from './pages/ia-conversation/ia-conversation.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'blog', component: BlogComponent },
  { path: 'weather', component: WeatherComponent },
  { 
    path: 'ia-conversation', 
    component: IaConversationComponent,
    canActivate: [AuthGuard]
  },
  // Other protected routes
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canActivate: [AuthGuard],
    data: { roles: ['admin'] }
  },
  { path: '**', redirectTo: '' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

12. Silent Check SSO Listener

Add this to your main component (AppComponent):

src/app/app.component.ts

typescript

Copy

Download

import { Component, OnInit } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  constructor(private keycloak: KeycloakService) {}

  ngOnInit(): void {
    this.setupSilentCheckSso();
  }

  private setupSilentCheckSso(): void {
    window.addEventListener('message', event => {
      if (event.origin !== window.location.origin) {
        return;
      }

      const { href } = event.data;
      if (!href) {
        return;
      }

      const silentCheckSsoIframe = this.keycloak.getKeycloakInstance().createLoginUrl({
        redirectUri: href,
        prompt: 'none'
      });

      window.location.href = silentCheckSsoIframe;
    });
  }
}

13. Environment Configuration

Update your Angular environments:

src/environments/environment.ts

typescript

Copy

Download

import { environment } from '../config/keycloak.config';

export const environment = {
  ...environment,
  // other environment variables
};

src/environments/environment.prod.ts

typescript

Copy

Download

import { environmentProd } from '../config/keycloak.config';

export const environment = {
  ...environmentProd,
  // other environment variables
};

14. Handling Token Refresh

Create a service to periodically refresh the token:

src/app/core/auth/token-refresh.service.ts

typescript

Copy

Download

import { Injectable, OnDestroy } from '@angular/core';
import { AuthService } from './auth.service';
import { interval, Subscription } from 'rxjs';
import { switchMap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class TokenRefreshService implements OnDestroy {
  private refreshSubscription: Subscription;

  constructor(private authService: AuthService) {
    this.startTokenRefresh();
  }

  private startTokenRefresh(): void {
    // Refresh token every 5 minutes (300 seconds)
    this.refreshSubscription = interval(300 * 1000).pipe(
      switchMap(() => this.authService.updateToken())
    ).subscribe();
  }

  ngOnDestroy(): void {
    if (this.refreshSubscription) {
      this.refreshSubscription.unsubscribe();
    }
  }
}

15. Error Handling for Token Issues

Create an error interceptor:

src/app/core/interceptors/error.interceptor.ts

typescript

Copy

Download

import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(
    private router: Router,
    private keycloak: KeycloakService
  ) {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          // Token might be expired, try to refresh
          this.keycloak.login();
        } else if (error.status === 403) {
          this.router.navigate(['/access-denied']);
        }

        return throwError(error);
      })
    );
  }
}

Don’t forget to add it to your providers in AppModule:

typescript

Copy

Download

{
  provide: HTTP_INTERCEPTORS,
  useClass: ErrorInterceptor,
  multi: true
}

Implementation Notes

  1. SSR Considerations: The solution handles SSR by disabling Keycloak on the server side while maintaining functionality on the client side.
  2. Token Management: Automatic token refresh is implemented to ensure seamless user experience.
  3. Security: The silent check SSO implementation provides a better user experience while maintaining security.
  4. Role-Based Access: The AuthGuard supports role-based access control for routes.
  5. API Integration: The HTTP interceptor automatically adds the bearer token to API requests.
  6. Error Handling: Comprehensive error handling for authentication and authorization issues.
  7. Professional Patterns: Uses modern Angular patterns like observables, services, and dependency injection.

Final Steps

  1. Configure your Keycloak realm and client settings to match your configuration.
  2. Set up the correct redirect URIs in your Keycloak client configuration.
  3. Test the flow thoroughly in development before deploying to production.
  4. Consider adding additional security measures like CSP headers in production.

This implementation provides a robust, professional solution for Keycloak integration in Angular 19 with SSR support, covering all aspects from authentication to authorization and API integration.

blog.newsletter.kicker

blog.newsletter.title

blog.newsletter.description