Implementing Authentication in Angular with Keycloak JS: A Complete Guide
Implementing Authentication in Angular with Keycloak JS: A Complete Guide
In this article, we'll explore how to integrate Keycloak authentication into an Angular application using the official keycloak-js adapter. We'll cover the complete implementation used in this project, from initialization to protected routes and HTTP interceptors.
Why Keycloak JS?
Keycloak is a powerful open-source Identity and Access Management solution. While there's a community wrapper called keycloak-angular, we chose to use the official keycloak-js adapter directly. As detailed in our ADR on choosing Keycloak JS over Angular-Keycloak, this gives us:
- Direct control and full visibility
- Framework-agnostic knowledge
- Official support from the Keycloak team
- Simpler dependencies and fewer breaking changes
π Official Keycloak JavaScript Adapter Documentation
Installation
First, install the Keycloak JS adapter:
npm install keycloak-js
π keycloak-js on NPM
Project Structure
Our authentication implementation consists of four main parts:
- AuthService - Manages Keycloak instance and core authentication logic
- HTTP Interceptor - Adds JWT tokens to API requests
- Route Guards - Protects routes requiring authentication
- App Initialization - Initializes Keycloak before the app starts
Step 1: Create the Auth Service
The AuthService is the core of our authentication system:
// src/app/shared/auth/auth.service.ts
import { Injectable, signal } from '@angular/core';
import Keycloak from 'keycloak-js';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private keycloakInstance!: Keycloak;
// Signals for reactive state
isAuthenticated = signal<boolean>(false);
userProfile = signal<Keycloak.KeycloakProfile | null>(null);
/**
* Initialize Keycloak instance
*/
async initialize(): Promise<boolean> {
this.keycloakInstance = new Keycloak({
url: environment.keycloakUrl,
realm: environment.keycloakRealm,
clientId: environment.keycloakClientId
});
try {
const authenticated = await this.keycloakInstance.init({
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/assets/silent-check-sso.html',
checkLoginIframe: false,
pkceMethod: 'S256' // Enable PKCE for better security
});
this.isAuthenticated.set(authenticated);
if (authenticated) {
await this.loadUserProfile();
}
// Token refresh setup
this.setupTokenRefresh();
return authenticated;
} catch (error) {
console.error('Keycloak initialization failed:', error);
return false;
}
}
/**
* Login user
*/
login(): Promise<void> {
return this.keycloakInstance.login();
}
/**
* Logout user
*/
logout(): Promise<void> {
this.isAuthenticated.set(false);
this.userProfile.set(null);
return this.keycloakInstance.logout();
}
/**
* Get current access token
*/
getToken(): string | undefined {
return this.keycloakInstance.token;
}
/**
* Update token if it's about to expire
*/
async updateToken(minValidity: number = 30): Promise<string | undefined> {
try {
const refreshed = await this.keycloakInstance.updateToken(minValidity);
if (refreshed) {
console.log('Token refreshed');
}
return this.keycloakInstance.token;
} catch (error) {
console.error('Failed to refresh token:', error);
await this.logout();
return undefined;
}
}
/**
* Check if user has a specific role
*/
hasRole(role: string): boolean {
return this.keycloakInstance.hasRealmRole(role);
}
/**
* Load user profile from Keycloak
*/
private async loadUserProfile(): Promise<void> {
try {
const profile = await this.keycloakInstance.loadUserProfile();
this.userProfile.set(profile);
} catch (error) {
console.error('Failed to load user profile:', error);
}
}
/**
* Setup automatic token refresh
*/
private setupTokenRefresh(): void {
// Refresh token every 30 seconds if expiring
setInterval(() => {
this.updateToken(60);
}, 30000);
}
}
π Keycloak JS API Reference
Key Methods Explained
init(): Initializes the Keycloak client with configuration optionsonLoad: 'check-sso': Checks if user is already logged in without forcing loginsilentCheckSsoRedirectUri: URL for silent SSO checks (iframe-based)pkceMethod: 'S256': Enables Proof Key for Code Exchange for enhanced security
updateToken(minValidity): Refreshes the token if it expires in less thanminValiditysecondshasRealmRole(role): Checks if the user has a specific role
Step 2: App Initialization
We need to initialize Keycloak before the Angular app starts. This ensures authentication state is ready when components load.
// src/app/app.config.ts
import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { AuthService } from './shared/auth/auth.service';
export function initializeKeycloak(authService: AuthService) {
return () => authService.initialize();
}
export const appConfig: ApplicationConfig = {
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeKeycloak,
deps: [AuthService],
multi: true
},
// ... other providers
]
};
The APP_INITIALIZER token ensures Keycloak is initialized before the app bootstraps.
Step 3: HTTP Interceptor
Add JWT tokens to all API requests automatically:
// src/app/shared/auth/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
// Only add token to API requests (not external URLs)
if (token && req.url.includes('/api/')) {
const clonedRequest = req.clone({
setHeaders: {
Authorization: `Bearer $${token}`
}
});
return next(clonedRequest);
}
return next(req);
};
Register the interceptor in app.config.ts:
// src/app/app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './shared/auth/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([authInterceptor])
),
// ... other providers
]
};
π Angular HTTP Interceptors
Step 4: Route Guards
Protect routes that require authentication:
// src/app/shared/auth/auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = async (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
// Redirect to login
await authService.login();
return false;
};
export const roleGuard = (role: string): CanActivateFn => {
return async (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated()) {
await authService.login();
return false;
}
if (!authService.hasRole(role)) {
// User doesn't have required role
router.navigate(['/unauthorized']);
return false;
}
return true;
};
};
Use guards in your routes:
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { authGuard, roleGuard } from './shared/auth/auth.guard';
export const routes: Routes = [
{
path: 'dashboard',
canActivate: [authGuard],
loadComponent: () => import('./dashboard/dashboard.component')
},
{
path: 'admin',
canActivate: [roleGuard('ROLE_ADMIN')],
loadComponent: () => import('./admin/admin.component')
}
];
π Angular Route Guards
Step 5: Silent SSO Check
Create a silent SSO check HTML file for background authentication:
<!-- 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>
This file enables Keycloak to silently check authentication status in an iframe.
π Keycloak Silent SSO
Using Authentication in Components
Now you can use authentication in your components:
// navbar.component.ts
import { Component, inject } from '@angular/core';
import { AuthService } from '../shared/auth/auth.service';
@Component({
selector: 'mysite-navbar',
template: `
<nav>
@if (authService.isAuthenticated()) {
<div>
<span>Welcome, {{ authService.userProfile()?.firstName }}</span>
<button (click)="authService.logout()">Logout</button>
</div>
} @else {
<button (click)="authService.login()">Login</button>
}
</nav>
`
})
export class NavbarComponent {
authService = inject(AuthService);
}
Environment Configuration
Configure Keycloak connection in your environment files:
// src/environments/environment.ts
export const environment = {
production: false,
keycloakUrl: 'http://localhost:8080',
keycloakRealm: 'my-realm',
keycloakClientId: 'my-angular-app'
};
// src/environments/environment.prod.ts
export const environment = {
production: true,
keycloakUrl: 'https://auth.mysite.com',
keycloakRealm: 'production-realm',
keycloakClientId: 'mysite-frontend'
};
Security Best Practices
1. Enable PKCE
Always use PKCE (Proof Key for Code Exchange) for enhanced security:
await this.keycloakInstance.init({
pkceMethod: 'S256'
});
π OAuth 2.0 PKCE
2. Token Refresh
Implement automatic token refresh to keep users logged in:
private setupTokenRefresh(): void {
setInterval(() => {
this.updateToken(60); // Refresh if expiring in 60 seconds
}, 30000); // Check every 30 seconds
}
3. Secure Token Storage
keycloak-js stores tokens in memory by default (not localStorage), which is more secure against XSS attacks.
4. HTTPS Only in Production
Always use HTTPS in production for Keycloak and your application.
Debugging and Troubleshooting
Enable Keycloak Logging
Keycloak({ enableLogging: true });
Common Issues
Issue: "Failed to initialize adapter"
- Check Keycloak URL and realm configuration
- Verify client exists in Keycloak admin console
- Check browser console for CORS errors
Issue: "Token refresh failed"
- Verify refresh token settings in Keycloak client configuration
- Check token expiration times
Issue: "Infinite redirect loop"
- Check
onLoadconfiguration - Verify redirect URIs in Keycloak client settings
π Keycloak Troubleshooting Guide
Conclusion
By using keycloak-js directly, we have full control over our authentication flow while benefiting from Keycloak's robust security features. This implementation is:
- Transparent: We see exactly what's happening
- Maintainable: Direct API usage is easier to debug
- Framework-agnostic: Knowledge transfers to other JavaScript frameworks
- Production-ready: Includes token refresh, role-based access, and security best practices
The initial setup requires more code than using a wrapper, but the long-term benefits of clarity, control, and maintainability make it worthwhile.
Further Reading
Comments (1)
User 71702d28
11/25/25, 1:20 PM
First comment to celebrate the new comment system. Having a counter for article visits would be nice as well.