Skip to main content

Angular

View a live deployed version.

info

We will use the Phase Two Angular example code here, but the logic could easily be applied to any existing application.

  1. Clone the Phase Two example repo.

  2. Open the Angular folder within /frameworks/angular. It's an Angular 22 app that leverages angular-oauth2-oidc to log users in with the authorization code flow and PKCE. You need Node.js 24 and pnpm.

  3. Point the app at your Keycloak. The OIDC settings come from the environment files in src/environments:

    • environment.development.ts is used by pnpm start and points to the local Keycloak from the examples repo.
    • environment.ts is used by production builds (pnpm build) and points to the hosted Phase Two demo realm.

    Both use the client ID angular, the example's client in the local Keycloak, so with the local Keycloak there's nothing to change. For another Keycloak, set oidcIssuerUri in src/environments/environment.development.ts to your realm's issuer URL and oidcClientId to the ID of your client:

    export const environment = {
    oidcIssuerUri: 'http://localhost:8080/auth/realms/p2examples',
    oidcClientId: 'angular',
    };

    For production builds, change src/environments/environment.ts the same way. The app runs in the browser, so it uses a public client and no client secret. It runs on port 4200, so the client needs http://localhost:4200/* as valid redirect URI, and + as web origin and as valid post logout redirect URI.

  4. Run pnpm install and then pnpm start.

  5. We'll review where we configure the OIDC client. Open the src/app/auth/auth.config.ts file. It builds the angular-oauth2-oidc settings from the environment file:

    import type { AuthConfig } from 'angular-oauth2-oidc';
    import { environment } from '../../environments/environment';

    export const authConfig: AuthConfig = {
    issuer: environment.oidcIssuerUri,
    clientId: environment.oidcClientId,
    redirectUri: `${window.location.origin}/`,
    postLogoutRedirectUri: `${window.location.origin}/`,
    responseType: 'code',
    scope: 'openid profile email',
    };

    Keycloak sends the user back to the app's origin after they log in and after they log out.

  6. The src/app/app.config.ts file registers angular-oauth2-oidc with provideOAuthClient(). provideAppInitializer runs AuthService.init() before the app renders, so the login completes first.

    import {
    type ApplicationConfig,
    inject,
    provideAppInitializer,
    provideBrowserGlobalErrorListeners,
    } from '@angular/core';
    import { provideHttpClient } from '@angular/common/http';
    import { provideOAuthClient } from 'angular-oauth2-oidc';
    import { AuthService } from './auth/auth.service';

    export const appConfig: ApplicationConfig = {
    providers: [
    provideBrowserGlobalErrorListeners(),
    provideHttpClient(),
    provideOAuthClient(),
    provideAppInitializer(() => inject(AuthService).init()),
    ],
    };
  7. The src/app/auth/auth.service.ts file configures the OAuthService with authConfig. It exposes the login state and the decoded tokens as signals, and the login and logout methods:

    import { computed, inject, Injectable, signal } from '@angular/core';
    import { toSignal } from '@angular/core/rxjs-interop';
    import { OAuthService } from 'angular-oauth2-oidc';
    import { jwtDecode } from 'jwt-decode';
    import { authConfig } from './auth.config';

    @Injectable({ providedIn: 'root' })
    export class AuthService {
    private readonly oauthService = inject(OAuthService);
    private readonly lastEvent = toSignal(this.oauthService.events, { initialValue: null });

    readonly error = signal<string | null>(null);

    readonly isAuthenticated = computed(() => {
    this.lastEvent();
    return this.oauthService.hasValidAccessToken();
    });

    readonly idTokenClaims = computed(() => {
    this.lastEvent();
    return this.oauthService.getIdentityClaims() as Record<string, unknown> | null;
    });

    readonly accessTokenClaims = computed(() => {
    this.lastEvent();
    const accessToken = this.oauthService.getAccessToken();
    return accessToken ? jwtDecode<Record<string, unknown>>(accessToken) : null;
    });

    async init(): Promise<void> {
    this.oauthService.configure(authConfig);
    this.oauthService.setupAutomaticSilentRefresh();
    try {
    await this.oauthService.loadDiscoveryDocumentAndTryLogin();
    } catch (error) {
    this.error.set(error instanceof Error ? error.message : 'Could not reach Keycloak');
    }
    }

    login(): void {
    this.oauthService.initCodeFlow();
    }

    logout(): void {
    this.oauthService.logOut();
    }
    }

    loadDiscoveryDocumentAndTryLogin() reads Keycloak's OpenID configuration and completes the login when Keycloak redirects back to the app. If Keycloak can't be reached, error holds the message instead. angular-oauth2-oidc keeps the tokens in session storage, and setupAutomaticSilentRefresh() refreshes them before they expire. The signals read lastEvent, so they update on every OAuthService event. logout() also ends the Keycloak session.

  8. The UserStatus component in src/app/user-status/user-status.ts injects the AuthService and turns the token claims into JSON for the template.

    import { Component, computed, inject } from '@angular/core';
    import { AuthService } from '../auth/auth.service';

    @Component({
    selector: 'app-user-status',
    templateUrl: './user-status.html',
    })
    export class UserStatus {
    protected readonly auth = inject(AuthService);

    protected readonly accessTokenJson = computed(() =>
    JSON.stringify(this.auth.accessTokenClaims() ?? {}, null, 2),
    );

    protected readonly idTokenJson = computed(() =>
    JSON.stringify(this.auth.idTokenClaims() ?? {}, null, 2),
    );

    protected readonly buttonClasses =
    'cursor-pointer rounded-md bg-indigo-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600';

    protected readonly textareaClasses =
    'block w-full rounded-md bg-purple-200/50 px-2 py-1.5 font-mono text-xs text-gray-900 ring-1 ring-gray-300 ring-inset';
    }
  9. Switching to the html template for the component, src/app/user-status/user-status.html, we can see how the login and logout buttons are rendered. @if blocks render them based on the user's authentication status from auth.isAuthenticated(), and show the message from auth.error() when the login fails.

    <div class="pb-8 text-xl italic">Your current status is:</div>
    @if (auth.isAuthenticated()) {
    <div class="mb-2 text-2xl text-p2blue-700">Authenticated</div>
    <div class="mb-6 text-p2blue-700">
    <div>{{ auth.idTokenClaims()?.['name'] }}</div>
    <div>{{ auth.idTokenClaims()?.['email'] }}</div>
    </div>
    <button type="button" [class]="buttonClasses" (click)="auth.logout()">Log out</button>
    <div class="mt-8 space-y-4 text-left">
    <div>
    <label for="access-token" class="mb-1 block text-sm font-semibold text-gray-900">
    Access token (decoded)
    </label>
    <textarea
    id="access-token"
    rows="12"
    readonly
    [class]="textareaClasses"
    [value]="accessTokenJson()"
    ></textarea>
    </div>
    <div>
    <label for="id-token" class="mb-1 block text-sm font-semibold text-gray-900">
    ID token (decoded)
    </label>
    <textarea
    id="id-token"
    rows="12"
    readonly
    [class]="textareaClasses"
    [value]="idTokenJson()"
    ></textarea>
    </div>
    </div>
    } @else if (auth.error()) {
    <div class="mb-2 text-2xl text-p2blue-700">Authentication error.</div>
    <div class="mb-6">{{ auth.error() }}</div>
    <button type="button" [class]="buttonClasses" (click)="auth.login()">Log in</button>
    } @else {
    <div class="mb-6 text-2xl text-p2blue-700">Not authenticated.</div>
    <button type="button" [class]="buttonClasses" (click)="auth.login()">Log in</button>
    }

    The logic using the AuthService to conditionally determine the authenticated state can be used to secure routes, components, and more.

  10. Open localhost:4200. You will see the Phase Two example landing page. Your current state should be Not authenticated. Click Log in. This will redirect you to your login page.

    info

    Use a non-admin user to sign in: demo / demo on the local Keycloak, or a user you added to your realm.

  11. Enter the credentials of the non-admin user and sign in. You will then be redirected to the application. The Phase Two example landing page now loads your Authenticated state, displaying your user's name and email, and the decoded access and ID tokens.

  12. Neat! Click Log out. The app also ends your Keycloak session, so the next Log in asks for your credentials again.