Implementing Authentication in Angular with Keycloak JS: A Complete Guide

November 5, 2025
Michael Kopp
angular keycloak security jwt oauth2 authentication

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:

  1. AuthService - Manages Keycloak instance and core authentication logic
  2. HTTP Interceptor - Adds JWT tokens to API requests
  3. Route Guards - Protects routes requiring authentication
  4. 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 options

    • onLoad: 'check-sso': Checks if user is already logged in without forcing login
    • silentCheckSsoRedirectUri: 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 than minValidity seconds

  • hasRealmRole(role): Checks if the user has a specific role

πŸ”— Keycloak Init Options

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.

πŸ”— Angular APP_INITIALIZER

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 onLoad configuration
  • 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)

71

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.