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.