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
- SSR Considerations: The solution handles SSR by disabling Keycloak on the server side while maintaining functionality on the client side.
- Token Management: Automatic token refresh is implemented to ensure seamless user experience.
- Security: The silent check SSO implementation provides a better user experience while maintaining security.
- Role-Based Access: The AuthGuard supports role-based access control for routes.
- API Integration: The HTTP interceptor automatically adds the bearer token to API requests.
- Error Handling: Comprehensive error handling for authentication and authorization issues.
- Professional Patterns: Uses modern Angular patterns like observables, services, and dependency injection.
Final Steps
- Configure your Keycloak realm and client settings to match your configuration.
- Set up the correct redirect URIs in your Keycloak client configuration.
- Test the flow thoroughly in development before deploying to production.
- 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.
