Integrating Keycloak with Angular: A Complete Guide

Guilliano Molaire Guilliano Molaire 13 min read

Adding authentication to an Angular application usually means choosing between building it yourself (slow, error-prone) or bolting on a third-party service that controls your user data. Keycloak offers a third path: a full-featured, open-source identity provider that you run on your own terms.

Integrating Keycloak with Angular requires three components: the keycloak-js adapter for OIDC communication, an APP_INITIALIZER to resolve authentication state before rendering, and a KeycloakBearerInterceptor to attach access tokens to outgoing HTTP requests.

This guide walks through a complete Keycloak integration with Angular 17+, using the modern standalone components pattern. By the end, you will have working authentication with login, logout, route protection, automatic token attachment, role-based access, and silent token refresh.

Prerequisites

Before starting, make sure you have the following:

  • Node.js 18+ and npm 9+ installed
  • Angular CLI 17+ (npm install -g @angular/cli)
  • A running Keycloak instance (version 22 or later). You can run one locally with Docker using the Skycloak Docker Compose Generator, or use a managed Keycloak provider like Skycloak.
  • Basic familiarity with Angular and TypeScript

If you need a quick local Keycloak instance for development, you can start one with Docker:

docker run -p 8080:8080 
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin 
  -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin 
  quay.io/keycloak/keycloak:26.0 start-dev

This gives you a Keycloak server at http://localhost:8080 with admin credentials admin/admin.

Configuring the Keycloak Client

Before touching any Angular code, you need to set up a client in Keycloak’s admin console.

Create a Realm

If you are not using the default master realm (and you should not for application workloads), create a new realm:

  1. Log into the Keycloak admin console at http://localhost:8080/admin
  2. Click the realm dropdown in the top-left corner and select Create realm
  3. Set the realm name to my-app (or whatever fits your project)
  4. Click Create

Create a Client

  1. Navigate to Clients in the left sidebar and click Create client
  2. Set the following on the General Settings step:
    • Client type: OpenID Connect
    • Client ID: angular-app
  3. On the Capability Config step:
    • Client authentication: Off (this makes it a public client, which is correct for SPAs)
    • Authorization: Off
    • Standard flow: Enabled
    • Direct access grants: Disabled (not needed for browser-based apps)
  4. On the Login Settings step, configure the redirect URIs:
    • Valid redirect URIs: http://localhost:4200/*
    • Valid post logout redirect URIs: http://localhost:4200/*
    • Web origins: http://localhost:4200

Click Save. Your Keycloak client is now ready.

Create a Test User

  1. Go to Users in the left sidebar and click Add user
  2. Set a username (e.g., testuser) and click Create
  3. Go to the Credentials tab, click Set password, and assign a password
  4. Toggle Temporary to Off so you are not forced to change it on first login

Add a Client Role (Optional)

If you plan to use role-based access control:

  1. Go to Clients, select angular-app, then click the Roles tab
  2. Click Create role and add a role called admin
  3. Go to Users, select your test user, click the Role mapping tab
  4. Click Assign role, filter by angular-app, and assign the admin role

Setting Up the Angular Project

Create a new Angular project with standalone components (the default in Angular 17+):

ng new keycloak-angular-demo --style=scss --routing --ssr=false
cd keycloak-angular-demo

Install the required packages:

npm install keycloak-angular keycloak-js

The keycloak-js package is the official Keycloak JavaScript adapter. The keycloak-angular package provides Angular-specific utilities including an HTTP interceptor and route guard.

Keycloak Initialization

The Keycloak adapter needs to initialize before your Angular app renders any components. This ensures that authentication state is resolved before any route guard or component tries to access it.

Create the Keycloak Configuration

Create a file to hold your Keycloak connection settings:

// src/app/keycloak.config.ts

export const keycloakConfig = {
  url: 'http://localhost:8080',
  realm: 'my-app',
  clientId: 'angular-app',
};

In a real project, you would pull these values from Angular’s environment files (environment.ts / environment.prod.ts) so they can differ between development and production.

Create the Initialization Function

Create a factory function that initializes Keycloak during app startup:

// src/app/init-keycloak.ts

import { KeycloakService } from 'keycloak-angular';
import { keycloakConfig } from './keycloak.config';

export function initializeKeycloak(keycloak: KeycloakService): () => Promise<boolean> {
  return () =>
    keycloak.init({
      config: keycloakConfig,
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri:
          window.location.origin + '/assets/silent-check-sso.html',
        checkLoginIframe: false,
      },
      enableBearerInterceptor: true,
      bearerPrefix: 'Bearer',
    });
}

A few important decisions in this configuration:

  • onLoad: 'check-sso' checks whether the user already has an active Keycloak session without forcing a login redirect. If you want to require login immediately on every page load, use 'login-required' instead.
  • silentCheckSsoRedirectUri points to a small HTML file used for silent session checks via a hidden iframe. This avoids full-page redirects when checking session status.
  • checkLoginIframe: false disables the legacy login iframe check, which can cause issues with modern browsers that block third-party cookies.

Create the Silent Check SSO Page

Create a small HTML file that Keycloak uses for silent session verification:

<!-- src/assets/silent-check-sso.html -->
<!doctype html>
<html>
  <body>
    <script>
      parent.postMessage(location.href, location.origin);
    </script>
  </body>
</html>

This file is loaded in a hidden iframe. It posts the redirect URL back to the parent window so Keycloak can determine the session state without a visible redirect.

Wire Up APP_INITIALIZER

In Angular’s standalone bootstrap pattern, you configure providers in app.config.ts:

// src/app/app.config.ts

import { ApplicationConfig, APP_INITIALIZER } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { KeycloakService, KeycloakBearerInterceptor } from 'keycloak-angular';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { routes } from './app.routes';
import { initializeKeycloak } from './init-keycloak';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withInterceptorsFromDi()),
    KeycloakService,
    {
      provide: APP_INITIALIZER,
      useFactory: initializeKeycloak,
      multi: true,
      deps: [KeycloakService],
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: KeycloakBearerInterceptor,
      multi: true,
    },
  ],
};

The APP_INITIALIZER token tells Angular to run initializeKeycloak before the application finishes bootstrapping. The multi: true flag allows it to coexist with other initializers. The KeycloakBearerInterceptor is also registered here — more on that in the HTTP interceptor section below.

Protecting Routes with an Auth Guard

Not every route in your application should be accessible without authentication. Keycloak-angular provides a base guard class that you can extend.

Create the Auth Guard

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

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } 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,
    state: RouterStateSnapshot
  ): Promise<boolean> {
    if (!this.authenticated) {
      await this.keycloak.login({
        redirectUri: window.location.origin + state.url,
      });
      return false;
    }

    // Check for required roles if specified in route data
    const requiredRoles = route.data['roles'] as string[];

    if (!requiredRoles || requiredRoles.length === 0) {
      return true;
    }

    return requiredRoles.every((role) => this.roles.includes(role));
  }
}

This guard does two things: it redirects unauthenticated users to the Keycloak login page, and it checks for role requirements defined in route data.

Configure Protected 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: 'dashboard',
    loadComponent: () =>
      import('./pages/dashboard/dashboard.component').then(
        (m) => m.DashboardComponent
      ),
    canActivate: [AuthGuard],
  },
  {
    path: 'admin',
    loadComponent: () =>
      import('./pages/admin/admin.component').then((m) => m.AdminComponent),
    canActivate: [AuthGuard],
    data: { roles: ['admin'] },
  },
];

The dashboard route requires authentication but no specific role. The admin route requires both authentication and the admin client role. For a similar approach in React, see our guide on securing a React app with Keycloak OIDC and PKCE.

HTTP Interceptor for Bearer Tokens

When your Angular app calls backend APIs, those APIs need to verify the caller’s identity. The standard approach with OIDC is to include the access token in the Authorization header of each HTTP request.

The KeycloakBearerInterceptor (registered in app.config.ts above) handles this automatically. It intercepts outgoing HTTP requests and attaches the bearer token.

By default, the interceptor adds the token to all requests. You can control which URLs receive the token by configuring the bearerExcludedUrls option during initialization:

// In init-keycloak.ts, add to the keycloak.init() call:
keycloak.init({
  config: keycloakConfig,
  initOptions: {
    onLoad: 'check-sso',
    silentCheckSsoRedirectUri:
      window.location.origin + '/assets/silent-check-sso.html',
    checkLoginIframe: false,
  },
  enableBearerInterceptor: true,
  bearerPrefix: 'Bearer',
  bearerExcludedUrls: [
    '/assets',
    'https://public-api.example.com',
  ],
});

Any request to a URL matching an entry in bearerExcludedUrls will not have the token attached. This is useful for public APIs or static asset requests where sending credentials is unnecessary.

Verifying Token Attachment

You can verify the interceptor is working by inspecting network requests in your browser’s developer tools or by pasting the token into the JWT Token Analyzer. Authenticated requests should include a header like:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Accessing User Profile and Roles

Once authentication is established, you will often need to display user information or make authorization decisions in your components.

Create a User Profile Component

// src/app/pages/dashboard/dashboard.component.ts

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

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="dashboard">
      <h1>Dashboard</h1>

      <div *ngIf="userProfile">
        <h2>Welcome, {{ userProfile.firstName }} {{ userProfile.lastName }}</h2>
        <p>Email: {{ userProfile.email }}</p>
        <p>Username: {{ userProfile.username }}</p>
      </div>

      <div>
        <h3>Your Roles</h3>
        <ul>
          <li *ngFor="let role of userRoles">{{ role }}</li>
        </ul>
      </div>

      <div *ngIf="hasAdminRole">
        <h3>Admin Panel</h3>
        <p>You have administrative access.</p>
      </div>

      <button (click)="logout()">Logout</button>
    </div>
  `,
})
export class DashboardComponent implements OnInit {
  userProfile: KeycloakProfile | null = null;
  userRoles: string[] = [];
  hasAdminRole = false;

  constructor(private readonly keycloak: KeycloakService) {}

  async ngOnInit(): Promise<void> {
    if (this.keycloak.isLoggedIn()) {
      this.userProfile = await this.keycloak.loadUserProfile();
      this.userRoles = this.keycloak.getUserRoles();
      this.hasAdminRole = this.userRoles.includes('admin');
    }
  }

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

The KeycloakService exposes several useful methods:

  • isLoggedIn() returns true if the user has an active session
  • loadUserProfile() fetches the full user profile from Keycloak (makes a network call)
  • getUserRoles() returns an array of all roles (both realm and client roles)
  • getUsername() returns the username from the token without a network call
  • getToken() returns the raw access token string

Accessing Token Claims Directly

Sometimes you need claims that are not part of the standard profile. You can decode the token to access custom attributes:

// Access the parsed token directly
const tokenParsed = this.keycloak.getKeycloakInstance().tokenParsed;

if (tokenParsed) {
  const email = tokenParsed['email'];
  const customAttribute = tokenParsed['custom_claim'];
  const realmRoles = tokenParsed['realm_access']?.roles || [];
  const clientRoles =
    tokenParsed['resource_access']?.['angular-app']?.roles || [];
}

Implementing Logout

Logout with Keycloak involves ending both the local session (clearing tokens in the browser) and the Keycloak server session.

Basic Logout

The simplest logout redirects the user to Keycloak’s logout endpoint, which invalidates the session and redirects back to your app:

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

The string argument is the URL where the user lands after logout. Make sure this URL is listed in the Valid post logout redirect URIs in your Keycloak client configuration.

Logout with Confirmation

For a better user experience, you might want to confirm before logging out:

confirmLogout(): void {
  if (confirm('Are you sure you want to log out?')) {
    this.keycloak.logout(window.location.origin);
  }
}

Navigation Bar with Auth State

Here is a practical navigation component that adapts based on authentication state:

// src/app/components/navbar/navbar.component.ts

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

@Component({
  selector: 'app-navbar',
  standalone: true,
  imports: [CommonModule, RouterModule],
  template: `
    <nav>
      <a routerLink="/">Home</a>
      <a *ngIf="isLoggedIn" routerLink="/dashboard">Dashboard</a>
      <a *ngIf="isAdmin" routerLink="/admin">Admin</a>

      <div class="auth-actions">
        <button *ngIf="!isLoggedIn" (click)="login()">Login</button>
        <span *ngIf="isLoggedIn">{{ username }}</span>
        <button *ngIf="isLoggedIn" (click)="logout()">Logout</button>
      </div>
    </nav>
  `,
})
export class NavbarComponent implements OnInit {
  isLoggedIn = false;
  isAdmin = false;
  username = '';

  constructor(private readonly keycloak: KeycloakService) {}

  ngOnInit(): void {
    this.isLoggedIn = this.keycloak.isLoggedIn();
    if (this.isLoggedIn) {
      this.username = this.keycloak.getUsername();
      this.isAdmin = this.keycloak.isUserInRole('admin');
    }
  }

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

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

Handling Token Refresh

Access tokens are short-lived by design (typically 5 minutes in Keycloak’s default configuration). When a token expires, the adapter needs to obtain a new one using the refresh token, without interrupting the user.

Automatic Token Refresh

The keycloak-js adapter handles token refresh internally when you make API calls through the KeycloakBearerInterceptor. However, if you want to proactively refresh tokens before they expire (for example, to avoid failed requests), you can set up a periodic refresh:

// src/app/services/token-refresh.service.ts

import { Injectable, OnDestroy } from '@angular/core';
import { KeycloakService } from 'keycloak-angular';

@Injectable({
  providedIn: 'root',
})
export class TokenRefreshService implements OnDestroy {
  private refreshInterval: ReturnType<typeof setInterval> | null = null;

  constructor(private readonly keycloak: KeycloakService) {}

  startTokenRefresh(intervalSeconds = 60): void {
    if (this.refreshInterval) {
      return;
    }

    this.refreshInterval = setInterval(async () => {
      if (!this.keycloak.isLoggedIn()) {
        this.stopTokenRefresh();
        return;
      }

      try {
        // Refresh if the token expires within the next 70 seconds
        const refreshed = await this.keycloak.updateToken(70);
        if (refreshed) {
          console.debug('Token was refreshed');
        }
      } catch (error) {
        console.error('Failed to refresh token, redirecting to login');
        this.keycloak.login();
      }
    }, intervalSeconds * 1000);
  }

  stopTokenRefresh(): void {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
      this.refreshInterval = null;
    }
  }

  ngOnDestroy(): void {
    this.stopTokenRefresh();
  }
}

Activate Token Refresh in Your App Component

// src/app/app.component.ts

import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import { NavbarComponent } from './components/navbar/navbar.component';
import { TokenRefreshService } from './services/token-refresh.service';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, NavbarComponent],
  template: `
    <app-navbar />
    <main>
      <router-outlet />
    </main>
  `,
})
export class AppComponent implements OnInit {
  constructor(
    private readonly keycloak: KeycloakService,
    private readonly tokenRefresh: TokenRefreshService
  ) {}

  ngOnInit(): void {
    if (this.keycloak.isLoggedIn()) {
      this.tokenRefresh.startTokenRefresh(60);
    }
  }
}

This checks every 60 seconds and refreshes the token if it will expire within the next 70 seconds. If the refresh fails (for example, because the refresh token itself has expired), the user is redirected to the login page.

Handling Token Expiry Events

You can also listen for Keycloak events to react to token lifecycle changes:

// Subscribe to Keycloak events in a component or service
const keycloakInstance = this.keycloak.getKeycloakInstance();

keycloakInstance.onTokenExpired = () => {
  console.debug('Token expired, attempting refresh...');
  keycloakInstance
    .updateToken(30)
    .then((refreshed) => {
      if (refreshed) {
        console.debug('Token refreshed successfully');
      }
    })
    .catch(() => {
      console.error('Token refresh failed');
      this.keycloak.login();
    });
};

keycloakInstance.onAuthLogout = () => {
  console.debug('User session ended');
  // Handle session timeout - redirect to login or show a message
};

Environment-Specific Configuration

For production deployments, extract your Keycloak settings into Angular environment files:

// src/environments/environment.ts (development)
export const environment = {
  production: false,
  keycloak: {
    url: 'http://localhost:8080',
    realm: 'my-app',
    clientId: 'angular-app',
  },
};
// src/environments/environment.prod.ts (production)
export const environment = {
  production: true,
  keycloak: {
    url: 'https://auth.yourdomain.com',
    realm: 'my-app',
    clientId: 'angular-app',
  },
};

Then update your config to use these values:

// src/app/keycloak.config.ts
import { environment } from '../environments/environment';

export const keycloakConfig = environment.keycloak;

In production, remember to update your Keycloak client’s redirect URIs to match your production domain.

Common Issues and Troubleshooting

CORS Errors

If you see CORS errors in the browser console, verify that the Web Origins setting in your Keycloak client configuration includes your Angular app’s origin. Setting it to + (plus sign) will allow all origins that match valid redirect URIs.

Infinite Redirect Loops

This usually happens when onLoad is set to 'login-required' and the redirect URI is not properly configured in Keycloak. Double-check that your Valid redirect URIs include a wildcard path: http://localhost:4200/*.

Token Not Attached to Requests

Make sure you have registered the KeycloakBearerInterceptor in your providers and that you are using provideHttpClient(withInterceptorsFromDi()) rather than the plain provideHttpClient(). The DI-based interceptor setup is required for class-based interceptors like the one from keycloak-angular.

“Login required” Errors After Deployment

When moving from development to a hosted Keycloak instance, the most common mistake is forgetting to update the Keycloak URL in your environment config, or not adding the production redirect URIs to the client configuration. Verify both before troubleshooting further.

Production Considerations

A few things to keep in mind when taking this integration to production:

  • Use HTTPS everywhere. Keycloak should run behind TLS in production, and your Angular app should be served over HTTPS as well. Tokens sent over unencrypted connections can be intercepted.
  • Set appropriate token lifetimes. The default access token lifetime of 5 minutes works well for most applications. Adjust the refresh token lifetime based on how long you want sessions to last.
  • Configure Content Security Policy headers. Your CSP needs to allow frame-src for the Keycloak domain if you use the silent SSO check iframe.
  • Monitor your Keycloak instance. Authentication infrastructure is critical path. Downtime means no one can log in. See our guide on monitoring Keycloak with Prometheus and Grafana for setting up observability.
  • Configure MFA for sensitive operations. Add multi-factor authentication to protect admin routes and high-value transactions.

Simplifying Keycloak Operations with Managed Hosting

Running Keycloak in production requires attention to high availability, database backups, version upgrades, and security patching. If you would rather focus on building your application than managing identity infrastructure, a managed Keycloak service can handle the operational burden.

Skycloak provides fully managed Keycloak hosting with automatic updates, monitoring, and multi-region availability. Your Angular application connects to Skycloak exactly the same way as shown in this guide — you just point your configuration to your Skycloak instance URL instead of a self-hosted one.

Summary

This guide covered the full integration path between Keycloak and a modern Angular application:

  1. Keycloak client configuration with proper OIDC settings for a single-page application
  2. App initialization using APP_INITIALIZER to resolve auth state before rendering
  3. Route protection with a custom AuthGuard supporting role-based access
  4. Automatic token attachment via the KeycloakBearerInterceptor
  5. User profile and role access in components
  6. Logout with proper session termination
  7. Token refresh to maintain sessions without user interruption

The keycloak-angular and keycloak-js libraries do most of the heavy lifting. The main work on your end is configuration — making sure URLs, redirect URIs, and client settings are consistent between your Angular app and the Keycloak server.

Ready to skip the infrastructure management and focus on your Angular app? Check out Skycloak’s managed Keycloak hosting plans and have a production-ready identity provider running in minutes.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman