Angular Integration
Angular Integration
This guide covers how to integrate Skycloak authentication into Angular applications using the Keycloak Angular adapter and Angular’s built-in security features.
ℹ️
Version Compatibility
-
keycloak-angular v19+ requires Angular 17+ and uses the new functional configuration with
provideKeycloak - keycloak-angular v18.x supports Angular 15-16 and uses the legacy module-based approach
- This guide covers both approaches below
Prerequisites
- Angular 17+ application (recommended), or Angular 15-16 for legacy setup
- Node.js 18+
- Skycloak cluster with configured realm and client
- Basic understanding of Angular services and guards
Quick Start (Angular 17+ with Standalone)
This is the recommended approach for Angular 17+ applications using standalone components.
1. Install Dependencies
npm install keycloak-angular keycloak-js2. Configure with provideKeycloak
For Angular 17+ with standalone configuration:
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideKeycloak, withAutoRefreshToken, AutoRefreshTokenService, UserActivityService, INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG } from 'keycloak-angular';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
provideKeycloak({
config: {
url: 'https://your-cluster-id.app.skycloak.io',
realm: 'your-realm',
clientId: 'your-angular-app',
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html',
checkLoginIframe: false,
},
features: [
withAutoRefreshToken({
onInactivityTimeout: 'logout',
sessionTimeout: 60000,
}),
],
providers: [AutoRefreshTokenService, UserActivityService],
}),
{
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: {
allowedList: [/\/api\/.*/],
excludedList: ['/assets', '/public'],
},
},
],
});3. Create Functional Auth Guard
// src/app/guards/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router, ActivatedRouteSnapshot } from '@angular/router';
import Keycloak from 'keycloak-js';
export const authGuard: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
const keycloak = inject(Keycloak);
const router = inject(Router);
const isAuthenticated = keycloak.authenticated ?? false;
if (!isAuthenticated) {
await keycloak.login({
redirectUri: window.location.origin + '/' + route.url.join('/'),
});
return false;
}
// Check for required roles
const requiredRoles = route.data['roles'] as string[] | undefined;
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const userRoles = keycloak.realmAccess?.roles ?? [];
const hasRequiredRoles = requiredRoles.every(role => userRoles.includes(role));
if (!hasRequiredRoles) {
return router.createUrlTree(['/access-denied']);
}
return true;
};4. Configure Routes
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{ path: '', loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent) },
{ path: 'public', loadComponent: () => import('./pages/public/public.component').then(m => m.PublicComponent) },
{
path: 'profile',
loadComponent: () => import('./pages/profile/profile.component').then(m => m.ProfileComponent),
canActivate: [authGuard],
},
{
path: 'admin',
loadComponent: () => import('./pages/admin/admin.component').then(m => m.AdminComponent),
canActivate: [authGuard],
data: { roles: ['admin'] },
},
{ path: '**', redirectTo: '' },
];Legacy Setup (Angular 15-16 with NgModules)
For applications using the legacy module-based approach with Angular 15-16 or keycloak-angular v18.x.
1. Install Dependencies
npm install keycloak-angular@18 keycloak-js2. Initialize Keycloak (Legacy)
Create a Keycloak initialization function:
// src/app/init/keycloak-init.factory.ts
import { KeycloakService } from 'keycloak-angular';
export function initializeKeycloak(keycloak: KeycloakService) {
return () =>
keycloak.init({
config: {
url: 'https://your-cluster-id.app.skycloak.io',
realm: 'your-realm',
clientId: 'your-angular-app',
},
initOptions: {
onLoad: 'check-sso',
silentCheckSsoRedirectUri:
window.location.origin + '/assets/silent-check-sso.html',
checkLoginIframe: false,
},
enableBearerInterceptor: true,
bearerPrefix: 'Bearer',
bearerExcludedUrls: ['/assets', '/public'],
});
}3. Configure App Module (Legacy)
// src/app/app.module.ts
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { initializeKeycloak } from './init/keycloak-init.factory';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule,
KeycloakAngularModule,
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
multi: true,
deps: [KeycloakService],
},
],
bootstrap: [AppComponent],
})
export class AppModule {}4. Create Auth Guard
// src/app/guards/auth.guard.ts
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root',
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected override readonly router: Router,
protected readonly keycloak: KeycloakService
) {
super(router, keycloak);
}
async isAccessAllowed(
route: ActivatedRouteSnapshot
): Promise<boolean | UrlTree> {
// Force login if not authenticated
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin + route.url.join('/'),
});
return false;
}
// Check for required roles
const requiredRoles = route.data['roles'];
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
// Check if user has required roles
const hasRequiredRoles = requiredRoles.every((role: string) =>
this.roles.includes(role)
);
if (!hasRequiredRoles) {
// Navigate to access denied page
return this.router.createUrlTree(['/access-denied']);
}
return true;
}
}5. Protected Routes
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './guards/auth.guard';
import { HomeComponent } from './components/home/home.component';
import { AdminComponent } from './components/admin/admin.component';
import { ProfileComponent } from './components/profile/profile.component';
import { PublicComponent } from './components/public/public.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'public', component: PublicComponent },
{
path: 'profile',
component: ProfileComponent,
canActivate: [AuthGuard],
},
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard],
data: { roles: ['admin'] },
},
{ path: '**', redirectTo: '' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}Authentication Service
Enhanced Auth Service
// src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakProfile, KeycloakTokenParsed } from 'keycloak-js';
import { Observable, from, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
export interface UserInfo {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
roles: string[];
groups: string[];
attributes: { [key: string]: any };
}
@Injectable({
providedIn: 'root',
})
export class AuthService {
constructor(private keycloakService: KeycloakService) {}
public getLoggedUser(): Observable<UserInfo | null> {
if (!this.keycloakService.isLoggedIn()) {
return of(null);
}
return from(this.keycloakService.loadUserProfile()).pipe(
map((profile: KeycloakProfile) => {
const token = this.keycloakService.getKeycloakInstance().tokenParsed as KeycloakTokenParsed;
return {
id: profile.id || token.sub || '',
username: profile.username || '',
email: profile.email || '',
firstName: profile.firstName || '',
lastName: profile.lastName || '',
roles: this.keycloakService.getUserRoles(true),
groups: token['groups'] || [],
attributes: profile.attributes || {},
};
}),
catchError(() => of(null))
);
}
public login(redirectUri?: string): void {
this.keycloakService.login({ redirectUri });
}
public logout(redirectUri?: string): void {
this.keycloakService.logout(redirectUri);
}
public register(redirectUri?: string): void {
this.keycloakService.register({ redirectUri });
}
public async getToken(): Promise<string> {
try {
await this.keycloakService.updateToken(30);
return await this.keycloakService.getToken();
} catch (error) {
console.error('Failed to refresh token', error);
throw error;
}
}
public hasRole(role: string): boolean {
return this.keycloakService.isUserInRole(role);
}
public hasRealmRole(role: string): boolean {
return this.keycloakService.isUserInRole(role, 'realm');
}
public hasResourceRole(role: string, resource?: string): boolean {
return this.keycloakService.isUserInRole(role, resource);
}
public hasAnyRole(roles: string[]): boolean {
return roles.some(role => this.hasRole(role));
}
public hasAllRoles(roles: string[]): boolean {
return roles.every(role => this.hasRole(role));
}
}User Profile Component
// src/app/components/profile/profile.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService, UserInfo } from '../../services/auth.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-profile',
template: `
<div class="profile-container" *ngIf="userInfo$ | async as user">
<h2>User Profile</h2>
<div class="profile-info">
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<p><strong>Name:</strong> {{ user.firstName }} {{ user.lastName }}</p>
<div class="roles">
<h3>Roles</h3>
<ul>
<li *ngFor="let role of user.roles">{{ role }}</li>
</ul>
</div>
<div class="groups" *ngIf="user.groups.length > 0">
<h3>Groups</h3>
<ul>
<li *ngFor="let group of user.groups">{{ group }}</li>
</ul>
</div>
</div>
<button (click)="logout()" class="logout-btn">Logout</button>
</div>
`,
})
export class ProfileComponent implements OnInit {
userInfo$!: Observable<UserInfo | null>;
constructor(private authService: AuthService) {}
ngOnInit(): void {
this.userInfo$ = this.authService.getLoggedUser();
}
logout(): void {
this.authService.logout();
}
}HTTP Interceptor
Token Interceptor with Retry Logic
// src/app/interceptors/token.interceptor.ts
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor,
HttpErrorResponse,
} from '@angular/common/http';
import { Observable, throwError, BehaviorSubject } from 'rxjs';
import { catchError, filter, take, switchMap, finalize } from 'rxjs/operators';
import { KeycloakService } from 'keycloak-angular';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
private isRefreshing = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
constructor(private keycloakService: KeycloakService) {}
intercept(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
// Skip interceptor for excluded URLs
if (this.isExcludedUrl(request.url)) {
return next.handle(request);
}
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
return this.handle401Error(request, next);
}
return throwError(() => error);
})
);
}
private handle401Error(
request: HttpRequest<unknown>,
next: HttpHandler
): Observable<HttpEvent<unknown>> {
if (!this.isRefreshing) {
this.isRefreshing = true;
this.refreshTokenSubject.next(null);
return from(this.keycloakService.updateToken(30)).pipe(
switchMap((refreshed: boolean) => {
if (refreshed) {
this.refreshTokenSubject.next(refreshed);
return next.handle(request);
}
// Refresh failed, redirect to login
this.keycloakService.login();
return throwError(() => new Error('Token refresh failed'));
}),
catchError((err) => {
this.keycloakService.login();
return throwError(() => err);
}),
finalize(() => {
this.isRefreshing = false;
})
);
}
return this.refreshTokenSubject.pipe(
filter((token) => token !== null),
take(1),
switchMap(() => next.handle(request))
);
}
private isExcludedUrl(url: string): boolean {
const excludedUrls = ['/assets', '/public', 'i18n'];
return excludedUrls.some((excluded) => url.includes(excluded));
}
}Role-Based Components
Role Directive
// src/app/directives/has-role.directive.ts
import {
Directive,
Input,
OnInit,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { AuthService } from '../services/auth.service';
@Directive({
selector: '[appHasRole]',
})
export class HasRoleDirective implements OnInit {
@Input() appHasRole!: string | string[];
@Input() appHasRoleElse?: TemplateRef<any>;
constructor(
private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef,
private authService: AuthService
) {}
ngOnInit(): void {
const roles = Array.isArray(this.appHasRole)
? this.appHasRole
: [this.appHasRole];
if (this.authService.hasAnyRole(roles)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else if (this.appHasRoleElse) {
this.viewContainer.createEmbeddedView(this.appHasRoleElse);
} else {
this.viewContainer.clear();
}
}
}Usage in templates:
<!-- Show only for admins -->
<div *appHasRole="'admin'">
<button (click)="deleteUser()">Delete User</button>
</div>
<!-- Show for multiple roles -->
<div *appHasRole="['admin', 'moderator']">
<button (click)="moderateContent()">Moderate</button>
</div>
<!-- With else template -->
<div *appHasRole="'premium-user'; else standardUser">
<app-premium-features></app-premium-features>
</div>
<ng-template #standardUser>
<app-standard-features></app-standard-features>
</ng-template>Permission Service
// src/app/services/permission.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { map, catchError, shareReplay } from 'rxjs/operators';
interface Permission {
resource: string;
scopes: string[];
}
@Injectable({
providedIn: 'root',
})
export class PermissionService {
private permissions$?: Observable<Permission[]>;
constructor(private http: HttpClient) {}
loadPermissions(): Observable<Permission[]> {
if (!this.permissions$) {
this.permissions$ = this.http.get<Permission[]>('/api/permissions').pipe(
shareReplay(1),
catchError(() => of([]))
);
}
return this.permissions$;
}
hasPermission(resource: string, scope: string): Observable<boolean> {
return this.loadPermissions().pipe(
map(permissions => {
const permission = permissions.find(p => p.resource === resource);
return permission ? permission.scopes.includes(scope) : false;
})
);
}
canAccess(resource: string): Observable<boolean> {
return this.hasPermission(resource, 'read');
}
canEdit(resource: string): Observable<boolean> {
return this.hasPermission(resource, 'write');
}
canDelete(resource: string): Observable<boolean> {
return this.hasPermission(resource, 'delete');
}
}Silent Authentication
Silent Check SSO
Create src/assets/silent-check-sso.html:
<!DOCTYPE html>
<html>
<head>
<title>Silent SSO Check</title>
</head>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>Auto Token Refresh
// src/app/services/token-refresh.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class TokenRefreshService implements OnDestroy {
private destroy$ = new Subject<void>();
private refreshInterval = 4 * 60 * 1000; // 4 minutes
constructor(private keycloakService: KeycloakService) {}
startTokenRefresh(): void {
interval(this.refreshInterval)
.pipe(takeUntil(this.destroy$))
.subscribe(async () => {
try {
const refreshed = await this.keycloakService.updateToken(300);
if (refreshed) {
console.log('Token refreshed successfully');
}
} catch (error) {
console.error('Failed to refresh token:', error);
await this.keycloakService.login();
}
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}Testing
Mock Keycloak Service
// src/app/testing/mock-keycloak.service.ts
import { Injectable } from '@angular/core';
@Injectable()
export class MockKeycloakService {
private _isLoggedIn = true;
private _roles = ['user', 'admin'];
private _token = 'mock-token';
init(): Promise<boolean> {
return Promise.resolve(true);
}
isLoggedIn(): boolean {
return this._isLoggedIn;
}
getUserRoles(allRoles?: boolean): string[] {
return this._roles;
}
getToken(): Promise<string> {
return Promise.resolve(this._token);
}
updateToken(minValidity: number): Promise<boolean> {
return Promise.resolve(true);
}
login(options?: any): Promise<void> {
return Promise.resolve();
}
logout(redirectUri?: string): Promise<void> {
return Promise.resolve();
}
isUserInRole(role: string, resource?: string): boolean {
return this._roles.includes(role);
}
loadUserProfile(): Promise<any> {
return Promise.resolve({
id: 'test-user-123',
username: 'testuser',
email: '[email protected]',
firstName: 'Test',
lastName: 'User',
});
}
// Test helpers
setLoggedIn(loggedIn: boolean): void {
this._isLoggedIn = loggedIn;
}
setRoles(roles: string[]): void {
this._roles = roles;
}
}Component Tests
// src/app/components/profile/profile.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { KeycloakService } from 'keycloak-angular';
import { ProfileComponent } from './profile.component';
import { AuthService } from '../../services/auth.service';
import { MockKeycloakService } from '../../testing/mock-keycloak.service';
describe('ProfileComponent', () => {
let component: ProfileComponent;
let fixture: ComponentFixture<ProfileComponent>;
let authService: AuthService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ProfileComponent],
providers: [
AuthService,
{ provide: KeycloakService, useClass: MockKeycloakService },
],
}).compileComponents();
fixture = TestBed.createComponent(ProfileComponent);
component = fixture.componentInstance;
authService = TestBed.inject(AuthService);
});
it('should display user information', async () => {
fixture.detectChanges();
await fixture.whenStable();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.profile-info')).toBeTruthy();
expect(compiled.textContent).toContain('testuser');
expect(compiled.textContent).toContain('[email protected]');
});
it('should show roles', async () => {
fixture.detectChanges();
await fixture.whenStable();
const roles = fixture.nativeElement.querySelectorAll('.roles li');
expect(roles.length).toBe(2);
expect(roles[0].textContent).toContain('user');
expect(roles[1].textContent).toContain('admin');
});
});Production Configuration
Environment-Specific Config
// src/environments/environment.prod.ts
export const environment = {
production: true,
keycloak: {
url: 'https://auth.yourcompany.com',
realm: 'production',
clientId: 'angular-app',
},
apiUrl: 'https://api.yourcompany.com',
};
// src/environments/environment.ts
export const environment = {
production: false,
keycloak: {
url: 'https://dev-auth.yourcompany.com',
realm: 'development',
clientId: 'angular-app-dev',
},
apiUrl: 'http://localhost:8080',
};Error Handling
// src/app/services/error-handler.service.ts
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root',
})
export class ErrorHandlerService {
constructor(
private router: Router,
private keycloakService: KeycloakService
) {}
handleError(error: HttpErrorResponse): void {
switch (error.status) {
case 401:
// Unauthorized - refresh token or redirect to login
this.handleUnauthorized();
break;
case 403:
// Forbidden - redirect to access denied
this.router.navigate(['/access-denied']);
break;
case 404:
// Not found
this.router.navigate(['/not-found']);
break;
default:
// Generic error
console.error('An error occurred:', error);
}
}
private async handleUnauthorized(): Promise<void> {
try {
const refreshed = await this.keycloakService.updateToken(30);
if (!refreshed) {
await this.keycloakService.login();
}
} catch {
await this.keycloakService.login();
}
}
}Troubleshooting
Common Issues
-
Infinite Redirect Loop
- Check redirect URI configuration in Skycloak
- Verify
onLoadoption in init configuration - Ensure cookies are enabled
-
Token Not Attached to Requests
- Verify HTTP interceptor is provided
- Check bearer excluded URLs configuration
- Ensure interceptor order in providers
-
Role Check Failures
- Verify role mappings in Skycloak
- Check realm vs client role configuration
- Debug token claims in browser console
Debug Utilities
// src/app/utils/keycloak-debug.ts
export function debugKeycloak(keycloakService: KeycloakService): void {
const keycloak = keycloakService.getKeycloakInstance();
console.group('Keycloak Debug Info');
console.log('Authenticated:', keycloak.authenticated);
console.log('Subject:', keycloak.subject);
console.log('Token:', keycloak.token);
console.log('Parsed Token:', keycloak.tokenParsed);
console.log('Refresh Token:', keycloak.refreshToken);
console.log('Realm Roles:', keycloak.realmAccess?.roles);
console.log('Resource Roles:', keycloak.resourceAccess);
console.groupEnd();
}