Keycloak with Angular: Modern Integration Guide (vs MSAL)

Guilliano Molaire Guilliano Molaire 9 min read
keycloak angular

Last updated: July 2026

To integrate Keycloak with Angular in 2026, use keycloak-angular v22 and its provideKeycloak API: it bootstraps keycloak-js before your app renders, refreshes tokens automatically based on user activity, and attaches bearer tokens with a functional interceptor. The KeycloakService, KeycloakAuthGuard, and KeycloakBearerInterceptor classes that most tutorials still teach have been deprecated since v19. And if you got here comparing MSAL: MSAL only works with Microsoft Entra ID, so for Keycloak (or any standard OIDC provider) you want keycloak-js.

Why most Keycloak and Angular tutorials are outdated

Angular settled on standalone components, functional guards, and functional interceptors as the canonical patterns, and the ecosystem followed. The current stable release is Angular 22.0.7, with v21 in long-term support (Angular releases). Class-based interceptors registered through HTTP_INTERCEPTORS and guards that extend a base class are legacy at this point.

keycloak-angular kept pace. Version 22.0.0, released June 2026, supports Angular 22 and works with keycloak-js versions 18 through 26 (keycloak-angular on GitHub). Here is the part that matters, and the reason this guide got a full rewrite: KeycloakService, KeycloakAuthGuard, KeycloakBearerInterceptor, and KeycloakAngularModule have all been deprecated since v19. That deprecated API is exactly what almost every tutorial on the internet teaches. Including, in fairness, the previous version of this one.

The underlying adapter is in good shape too. Since version 26.2.0, keycloak-js lives in its own repository with independent semantic versioning, decoupled from Keycloak server releases, and the current release is 26.2.4 from April 2026, compatible with all supported Keycloak server versions (keycloak-js 26.2.4 release notes).

So the current stack is: Angular 22, keycloak-angular v22, keycloak-js 26.2.4. Everything below uses it.

New to Keycloak itself? Start with our complete Keycloak guide, then come back here.

What you need before you start

You will need a current Node LTS, the Angular CLI, and a running Keycloak 26.x instance. If you do not have one handy, Docker gets you there in one command:

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

In the admin console at http://localhost:8080/admin, create a realm called my-app, then create a client:

  • Client ID: angular-app, type OpenID Connect
  • Client authentication: Off. This makes it a public client, which is correct for SPAs. A browser app cannot keep a secret, so it should not have one.
  • Standard flow: On. Direct access grants: Off, browser apps have no business with password grants.
  • Valid redirect URIs and Valid post logout redirect URIs: http://localhost:4200/*
  • Web origins: http://localhost:4200

You do not need to configure anything for PKCE. keycloak-js uses PKCE with the S256 challenge method by default (Keycloak JavaScript adapter docs).

Create a test user with a password, and if you want to follow the role-based part below, add an admin role on the angular-app client and assign it to that user. Then install both packages in your Angular project:

npm install keycloak-angular keycloak-js

The modern setup: provideKeycloak in app.config.ts

One provider call replaces the whole old dance of KeycloakService, an APP_INITIALIZER factory, and HTTP_INTERCEPTORS registration:

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
  provideKeycloak,
  withAutoRefreshToken,
  AutoRefreshTokenService,
  UserActivityService,
  createInterceptorCondition,
  IncludeBearerTokenCondition,
  INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
  includeBearerTokenInterceptor,
} from 'keycloak-angular';

import { routes } from './app.routes';

// Only requests matching this pattern get the bearer token.
const apiCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
  urlPattern: /^(https://api.example.com)(/.*)?$/i,
});

export const appConfig: ApplicationConfig = {
  providers: [
    provideKeycloak({
      config: {
        url: 'http://localhost:8080',
        realm: 'my-app',
        clientId: 'angular-app', // public client, no secret
      },
      initOptions: {
        onLoad: 'check-sso',
        silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
      },
      features: [
        withAutoRefreshToken({
          onInactivityTimeout: 'logout',
          sessionTimeout: 300000, // log out after 5 minutes of inactivity
        }),
      ],
      providers: [AutoRefreshTokenService, UserActivityService],
    }),
    {
      provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
      useValue: [apiCondition],
    },
    provideRouter(routes),
    provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),
  ],
};

A few things worth understanding about what this does.

No more APP_INITIALIZER factory. When you pass initOptions, provideKeycloak wires up app initialization internally through Angular’s provideAppInitializer, so authentication state is resolved before your first component renders. You no longer write that factory function yourself.

check-sso instead of login-required. With check-sso, the adapter checks whether the user already has a Keycloak session without forcing a login redirect, so anonymous visitors can still see public pages. Use login-required only when the entire app is behind authentication.

The silent check-sso caveat. The silentCheckSsoRedirectUri points at a tiny static page loaded in a hidden iframe:

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

Two caveats here. First, new Angular projects serve static files from the public/ directory at the app root, not src/assets/, so the file above is reachable at /silent-check-sso.html. Second, per the adapter docs, when the browser blocks third-party cookies, silent check-sso automatically falls back to a regular redirect-based check-sso, and the session status iframe disables itself in browsers with restrictive cookie policies. A full-page redirect in Safari is expected behavior, not a bug in your setup.

Token refresh is handled for you. withAutoRefreshToken is aware of user activity: it keeps tokens fresh while the user is actually using the app and applies your sessionTimeout policy when they go idle. If your existing code has a hand-rolled setInterval calling updateToken(70), this feature is its replacement. Delete the service.

The bearer interceptor is an allow-list. The old KeycloakBearerInterceptor attached your access token to every outgoing request unless you remembered to exclude URLs with bearerExcludedUrls. The functional includeBearerTokenInterceptor inverts that: tokens are only attached to requests matching a createInterceptorCondition pattern. Your token goes to your API origin and nowhere else, which is the right default for something that grants access to your users’ data.

Building the same flow in React? We have the sibling guide: secure React API access using Keycloak, OIDC and PKCE.

Route protection with createAuthGuard

Guards are functional now too. createAuthGuard hands you an AuthGuardData object with the authentication state, the granted roles, and the keycloak-js instance:

// src/app/guards/auth.guard.ts
import { inject } from '@angular/core';
import {
  ActivatedRouteSnapshot,
  CanActivateFn,
  Router,
  RouterStateSnapshot,
  UrlTree,
} from '@angular/router';
import { AuthGuardData, createAuthGuard } from 'keycloak-angular';

const isAccessAllowed = async (
  route: ActivatedRouteSnapshot,
  state: RouterStateSnapshot,
  authData: AuthGuardData
): Promise<boolean | UrlTree> => {
  const { authenticated, grantedRoles, keycloak } = authData;

  if (!authenticated) {
    await keycloak.login({
      redirectUri: window.location.origin + state.url,
    });
    return false;
  }

  const requiredRole = route.data['role'] as string | undefined;
  if (!requiredRole) {
    return true;
  }

  const hasRole = Object.values(grantedRoles.resourceRoles).some((roles) =>
    roles.includes(requiredRole)
  );

  return hasRole ? true : inject(Router).parseUrl('/forbidden');
};

export const canActivateAuthRole =
  createAuthGuard<CanActivateFn>(isAccessAllowed);

Wire it into your routes the standard functional way:

// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { canActivateAuthRole } from './guards/auth.guard';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./dashboard/dashboard').then((m) => m.Dashboard),
    canActivate: [canActivateAuthRole],
  },
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin').then((m) => m.Admin),
    canActivate: [canActivateAuthRole],
    data: { role: 'admin' },
  },
];

Unauthenticated users get sent to the Keycloak login page and land back on the route they originally asked for. The admin route additionally requires the admin client role.

Inside components, there is no wrapper service anymore either. provideKeycloak registers the keycloak-js instance itself in the injector, so you inject it directly:

import { Component, inject } from '@angular/core';
import Keycloak from 'keycloak-js';

@Component({
  selector: 'app-navbar',
  template: `<button (click)="logout()">Logout</button>`,
})
export class Navbar {
  private readonly keycloak = inject(Keycloak);

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

Where should an Angular app keep its tokens?

In memory, and nowhere else. keycloak-js holds tokens in JavaScript memory, and the official docs are blunt about it: tokens should never be persisted. A page refresh drops them, which is exactly what check-sso plus the silent SSO check exists for. The adapter quietly re-establishes tokens from the Keycloak session cookie instead of reading anything out of localStorage.

The broader security guidance backs this up. The IETF’s OAuth 2.0 for Browser-Based Apps document (still an Internet-Draft at draft-27, not an RFC) ranks the available architectures: a backend-for-frontend that keeps tokens out of the browser entirely is strongest, a token-mediating backend comes next, and a pure browser-based OAuth client is the weakest of the three (draft-ietf-oauth-browser-based-apps). When tokens do live in the browser, the mitigations the draft calls for are the ones this setup already applies: authorization code flow with PKCE, in-memory storage, and short token lifetimes.

The other half of the story is your API. A bearer token is only as good as the server that verifies it, so your backend must check the signature against your realm’s JWKS, plus issuer, audience, and expiry. We cover that end to end in how to verify a Keycloak-issued access token on the backend, and you can sanity-check any token against your realm’s JWKS in the browser with our JWKS verifier.

MSAL or keycloak-js: pick by identity provider, not by library

A lot of people searching for Angular authentication land on MSAL tutorials, so let’s settle this honestly. @azure/msal-angular is at version 6.0.1 as of July 2026, supports Angular 22, wraps msal-browser v5, and implements the authorization code flow with PKCE. It is a well-maintained library. It is also built for exactly one identity platform: Microsoft Entra ID.

keycloak-angular + keycloak-js @azure/msal-angular
Built for Keycloak and standard OIDC Microsoft Entra ID only
Current version v22.0.0 (Jun 2026) / 26.2.4 (Apr 2026) 6.0.1 (Jul 2026)
Angular 22 support Yes Yes
Flow Authorization code + PKCE (S256 default) Authorization code + PKCE
Token storage In memory only Persistent cache, configurable cacheLocation
Using it against Keycloak Supported, first-class Unsupported

The storage difference is worth noticing: MSAL maintains a persistent token cache with a configurable cacheLocation, while keycloak-js deliberately keeps tokens in memory and relies on the SSO session for continuity. Different philosophies, both defensible for their own platforms.

The decision itself is simple:

  • Your identity provider is Entra ID: use MSAL. It is the supported path and genuinely good at Entra-specific behavior. Do not fight it.
  • Your identity provider is Keycloak, or any standard OIDC provider: use keycloak-js with keycloak-angular. That is what they are built and tested for.
  • Pointing MSAL at Keycloak: unsupported territory. You might get a login screen working, but you would be depending on behavior that neither Microsoft nor the Keycloak team tests, documents, or supports. When it breaks on an upgrade, nobody owes you a fix.

And if your real requirement is “our users sign in with Microsoft accounts,” you do not need MSAL for that. Keycloak can broker Entra ID as an upstream identity provider, so your Angular app only ever speaks keycloak-js while users still authenticate with their Microsoft credentials.

Troubleshooting the usual suspects

silent-check-sso.html returns 404. New Angular projects serve static files from public/, not src/assets/. If you copied the file location from an older tutorial, move it to public/silent-check-sso.html.

Full-page redirects where you expected silent checks. Third-party cookie blocking. As covered above, the adapter falls back to redirect-based check-sso by design when the iframe approach cannot work.

Tokens are not attached to API calls. The modern interceptor is allow-list based. Check that your urlPattern regex actually matches the full API origin, protocol included, and that INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG is provided.

CORS errors in the console. The client’s Web origins setting in Keycloak must include your app’s origin. Setting it to + mirrors your valid redirect URIs.

Taking it to production

The Angular side of this integration is genuinely done in one config file. The Keycloak side still needs the usual production care: TLS everywhere, short access token lifetimes so the auto-refresh feature earns its keep, backups, and upgrades that track the 26.x line. If you would rather not carry that pager, Skycloak runs managed Keycloak with single sign-on across your apps, and everything in this guide works unchanged. Point provideKeycloak at your realm URL and ship.

Frequently asked questions

Can I use MSAL to authenticate against Keycloak?

No, not in any supported way. MSAL libraries, including @azure/msal-angular, are built for the Microsoft identity platform (Entra ID) specifically. Keycloak is a standard OIDC provider, so use keycloak-js or keycloak-angular, which are built and tested for it.

Does keycloak-angular support Angular 22?

Yes. keycloak-angular v22.0.0, released in June 2026, supports Angular 22 and is compatible with keycloak-js versions 18 through 26. Version numbers now track Angular major versions, which makes compatibility easy to reason about.

What replaced KeycloakService and APP_INITIALIZER in keycloak-angular?

The provideKeycloak function, which registers app initialization internally through Angular’s provideAppInitializer when you pass initOptions. Components inject the keycloak-js Keycloak instance directly, route guards come from createAuthGuard, and token attachment uses the functional includeBearerTokenInterceptor. The old classes have been deprecated since v19.

Where should an Angular SPA store Keycloak access tokens?

In memory only. keycloak-js does this by default, and the official docs state tokens should never be persisted. If you need stronger guarantees, the OAuth 2.0 for Browser-Based Apps draft recommends a backend-for-frontend that keeps tokens out of the browser entirely.

Is keycloak-js still maintained now that it left the main Keycloak repo?

Yes, actively. Since version 26.2.0 it lives in its own repository with independent semantic versioning, which lets it ship faster than the server release cycle. The current release, 26.2.4 from April 2026, is compatible with all supported Keycloak server versions.

Tired of running Keycloak yourself?

Skycloak runs real upstream Keycloak for you with a 99.99% SLA. No fork, no lock-in, just managed Keycloak that stays patched and on call so you don't have to.

Guilliano Molaire
Written by
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.

Start Free Trial Talk to Sales
© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman