Authenticate your Angular App with Keycloak OpenID

Keycloak acts as the authorization server, issuing tokens. The Angular client uses standalone components for authentication and token management. The flow is Authorization Code Flow with PKCE for enhanced security.

Prerequisites

Node.js and npm are installed. Docker is installed on macOS or Docker Desktop for Windows users with WSL 2 enabled. Basic familiarity with Angular and TypeScript is assumed.

Tutorial

Set Up Keycloak

Open Terminal and run

docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:latest start-dev

Access Keycloak at http://localhost:8080 and log in with admin/admin. For Windows users, use PowerShell or Command Prompt, install Docker Desktop from docker.com and ensure WSL 2 is enabled, check port 8080 with

netstat -aon | findstr :8080

and allow it in Windows Defender Firewall if blocked. Go to the Master realm, click Create Realm, name it mydemo, and save. Navigate to Clients and select Create. Set Client ID to angular-client, Client Protocol to openid-connect, and Access Type to public. Save, then configure Valid Redirect URIs to http://localhost:4200/* and Web Origins to +. For Client Authentication, set to Off as default for public clients like Angular, as it doesn’t require a client secret. For Windows users, configure via the Keycloak UI with no difference. For Authorization, set to Off for this tutorial as we’re not using fine-grained access control; enable it for role-based access if needed later. For Authentication Flow, use Standard Flow, which supports Authorization Code Flow with PKCE, ideal for Angular; other flows like Implicit are less secure. For Direct Access Grants, leave Off as it enables Resource Owner Password Credentials Grant, unsuitable for public clients like Angular due to security risks. For Implicit Flow, set to Off as it is deprecated and less secure; stick with Authorization Code Flow. For Service Accounts Roles, leave Off unless you need a service account for machine-to-machine communication; enable and configure roles if required. For Standard Token Exchange, leave Off unless implementing token exchange between services; not needed here. For OAuth 2.0 Device Authorization Grant, leave Off unless supporting device flows like smart TVs; not relevant for browser-based Angular apps. For OIDC CIBA Grant, leave Off unless using Client Initiated Backchannel Authentication for out-of-band authentication; not applicable here. Note the Client ID angular-client. Go to Users and select Add User. Set username to testuser, save, and go to the Credentials tab. Set a password to password and disable Temporary. Open http://localhost:8080/realms/mydemo/account and log in to verify.

Set Up Angular Client with Standalone Components

Run

ng new angular-oauth --style=scss --routing=true --skip-tests

and

cd angular-oauth

Run

npm install angular-oauth2-oidc

to add the dependency. Create src/app/auth-config.ts with

import { OAuthConfig } from 'angular-oauth2-oidc';

export const oauthConfig: OAuthConfig = {
  issuer: 'http://localhost:8080/realms/mydemo',
  clientId: 'angular-client',
  responseType: 'code',
  scope: 'openid profile email',
  redirectUri: window.location.origin + '/callback',
  silentRefreshRedirectUri: window.location.origin + '/silent-refresh',
  requireHttps: false
};

Run

ng generate service auth --skip-tests --standalone

to generate a service. Edit src/app/auth.service.ts with

import { Injectable } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { oauthConfig } from './auth-config';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  constructor(private oauthService: OAuthService, private http: HttpClient) {
    this.configure();
  }

  private configure() {
    this.oauthService.configure(oauthConfig);
    this.oauthService.loadDiscoveryDocumentAndTryLogin().then(() => {
      if (!this.oauthService.hasValidAccessToken()) {
        this.oauthService.initCodeFlow();
      }
    });
  }

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

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

  getToken() {
    return this.oauthService.getAccessToken();
  }

  isLoggedIn() {
    return this.oauthService.hasValidAccessToken();
  }
}

Run

ng generate component app --skip-tests --standalone

to generate a root component. Create src/app/app.component.html with

<button *ngIf="!auth.isLoggedIn()" (click)="auth.login()">Login</button>
<button *ngIf="auth.isLoggedIn()" (click)="auth.logout()">Logout</button>
<p *ngIf="auth.isLoggedIn()">Token: {{ auth.getToken() }}</p>

Edit src/app/app.component.ts with

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

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterModule],
  templateUrl: './app.component.html'
})
export class AppComponent {
  auth = inject(AuthService);
}

Run

ng generate component callback --skip-tests --standalone

to generate a callback component. Create src/app/callback.component.html with

<p>Processing authentication...</p>

Edit src/app/callback.component.ts with

import { Component } from '@angular/core';

@Component({
  selector: 'app-callback',
  standalone: true,
  templateUrl: './callback.component.html'
})
export class CallbackComponent {}

Create src/app/app.routes.ts with

import { Routes } from '@angular/router';
import { AppComponent } from './app.component';
import { CallbackComponent } from './callback.component';

export const routes: Routes = [
  { path: '', component: AppComponent },
  { path: 'callback', component: CallbackComponent },
  { path: '**', redirectTo: '' }
];

Update src/main.ts with

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { provideHttpClient } from '@angular/common/http';
import { importProvidersFrom } from '@angular/core';
import { OAuthModule } from 'angular-oauth2-oidc';
import { oauthConfig } from './app/auth-config';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    importProvidersFrom(OAuthModule.forRoot(oauthConfig))
  ]
}).catch((err) => console.error(err));

Run

ng serve

to start the app. Open http://localhost:4200, click Login, and authenticate with Keycloak. For Windows users, use PowerShell or Command Prompt and allow port 4200 in Windows Defender Firewall if needed.

Troubleshooting

Ensure redirect URIs in Keycloak match http://localhost:4200/* for token errors. Verify Keycloak is running at http://localhost:8080. For Windows users, adjust firewall rules via Windows Defender if ports are blocked.

Leave a Reply