Skip to content
Ashish.
All posts
Diagram illustrating the trust boundary between an Angular SPA, the angular-oauth2-oidc library, and an Identity Provider.

Angular Authentication with OIDC: Using angular-oauth2-oidc Library

A guide to implementing secure single-page application authentication in Angular using the angular-oauth2-oidc library and OpenID Connect.

By Ashish Srivastava

The Architecture of Trust: Angular, OIDC, and angular-oauth2-oidc

When you integrate OpenID Connect (OIDC) into an Angular Single Page Application (SPA), you are not simply "logging in." You are orchestrating a state transfer between a browser context, a trusted identity provider (IdP), and your application's internal security context. The angular-oauth2-oidc library is not a magic wrapper; it is a specialized state machine that manages the complex redirect loops, token exchanges, and cryptographic nonces required to prove a user's identity without exposing credentials to your backend.

The Redirect Mechanism and PKCE

Consider a user named Alice accessing your Angular app. She is not authenticated. The application cannot magically know who she is. Instead, the library must initiate a handover.

In the legacy Implicit Flow, the IdP would return the access token directly in the URL fragment (#access_token=...). This is dangerous because URL fragments are often logged by proxies or saved in browser history. The modern standard, mandated by the library for SPAs, is the Authorization Code Flow with PKCE (Proof Key for Code Exchange).

Here is the mechanism:

  1. Code Challenge: The library generates a random code_verifier string in memory. It hashes this (SHA-256) to create a code_challenge.
  2. Redirect: The library redirects Alice to the IdP's authorization endpoint, passing the code_challenge and the client ID.
  3. Authentication: Alice logs in at the IdP. The IdP does not see the code_verifier, only the hash.
  4. Code Grant: The IdP redirects back to your Angular app with a short-lived code in the query string.
  5. Token Exchange: Your app (or the library acting on its behalf) sends the code and the original code_verifier to the IdP's token endpoint. The IdP verifies the hash matches. If it does, it issues the Access Token and ID Token.

This ensures that even if an attacker intercepts the redirect, they cannot exchange the code for a token without the code_verifier, which never left the browser.

// src/app/app.config.ts
import { provideOAuthClient } from 'angular-oauth2-oidc';
import { OAuthConfig } from 'angular-oauth2-oidc';
 
const oauthConfig: OAuthConfig = {
  issuer: 'https://keycloak.example.com/realms/my-realm',
  redirectUri: window.location.origin + '/home',
  clientId: 'my-angular-app',
  scope: 'openid profile email roles',
  showDebugInformation: true, // Only for development
  strictDiscoveryDocumentValidation: false, // Set true in production to strictly validate the discovery document fields against the OpenID Connect spec.
  requireHttps: true
};
 
export const appConfig = [
  provideOAuthClient(oauthConfig)
];

Configuration as State Machine Definition

The OAuthConfig object in angular-oauth2-oidc is often misunderstood as a simple connection string. In reality, it defines the boundaries of the trust chain.

The issuer is critical. The library fetches the .well-known/openid-configuration document from this URL. This document contains the dynamic endpoints (Authorization, Token, Userinfo, JWKs) for the IdP. If you hardcode these endpoints, you lose the ability to handle IdP migrations or cluster failovers automatically.

Crucially, the storageStrategy determines where the library persists the tokens. Recent versions of angular-oauth2-oidc default to sessionStorage rather than LocalStorage. While LocalStorage remains available, it is vulnerable to XSS (Cross-Site Scripting) attacks. If your application has an XSS vulnerability, a malicious script can read the tokens from LocalStorage.

While angular-oauth2-oidc does not enforce a specific storage backend, the library's design assumes the storage is accessible. For high-security applications, you must implement a custom Storage service that writes to sessionStorage (cleared on tab close) or, ideally, utilizes HttpOnly cookies managed by your backend to prevent JavaScript access entirely. The library supports this via the storage property in the configuration, but the mechanism of how the tokens are retrieved and stored remains your responsibility.

// src/app/auth.service.ts
import { Injectable } from '@angular/core';
import { AuthService, OAuthState } from 'angular-oauth2-oidc';
 
@Injectable({ providedIn: 'root' })
export class MyAuthService extends AuthService {
  constructor(authService: AuthService) {
    super(authService);
  }
 
  // Hook into the token acquisition flow
  async configure() {
    await super.configure();
    // Custom logic to validate the ID token signature using the public key
    // fetched from the discovery document
    this.registerInterceptor();
  }
 
  registerInterceptor() {
    // The library provides a default interceptor, but we can extend it
    // to add custom claims or handle specific refresh token logic
  }
}

Token Handling and the HttpClient Interceptor

Once Alice successfully authenticates, the IdP returns an ID Token (JWT containing user claims) and an Access Token (JWT containing scopes). The angular-oauth2-oidc library parses these tokens and stores them.

The mechanism for securing API calls is the HttpInterceptor. When you make an HTTP request from Angular, the interceptor checks if an Access Token exists in the library's internal store. If it does, it injects the Authorization: Bearer <token> header into the request.

This is where the "blind trust" model of SPAs can fail. The library does not validate the token's signature on every request. It assumes the token is valid until it expires. If the token is stolen, the attacker can use it until it expires or the server revokes it.

To handle token expiration gracefully, the library exposes an isAuthenticated$ observable. This stream emits true or false based on the current token state. You can subscribe to this in your AuthGuard or CanActivate route guards.

// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from 'angular-oauth2-oidc';
 
@Component({
  selector: 'app-root',
  template: `
    <nav *ngIf="authService.isAuthenticated$ | async">
      Welcome, {{ authService.getUserData()?.name }}
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent implements OnInit {
  constructor(public authService: AuthService) {}
 
  ngOnInit() {
    // The library automatically handles the silent renewal (refresh token)
    // if configured correctly with an IdP that supports it.
    // If the token expires, the library attempts to refresh it silently.
    // If that fails, it triggers the login flow.
    this.authService.setupAutomaticSilentRefresh();
  }
}

Handling Keycloak Specifics and RBAC

Keycloak, a popular open-source identity provider, adds specific complexity to the OIDC flow. While Keycloak is OIDC compliant, it often includes custom claims like realm_access and resource_access which contain role information.

The standard angular-oauth2-oidc library handles standard OIDC claims automatically. However, non-standard claims like Keycloak's realm_access require manual parsing logic. You must manually parse the id_token or access_token to extract these specific roles.

Consider a scenario where Alice needs access to the "Admin" panel. The id_token from Keycloak might look like this in the payload:

{
  "sub": "alice-uuid",
  "email": "alice@example.com",
  "realm_access": {
    "roles": ["user", "admin"]
  },
  "resource_access": {
    "my-client": {
      "roles": ["viewer"]
    }
  }
}

The angular-oauth2-oidc library provides a getUserData() method which returns the claims from the ID token. You can write a utility function to extract these roles and store them in a local state service.

// src/app/user.service.ts
import { Injectable } from '@angular/core';
import { AuthService } from 'angular-oauth2-oidc';
 
@Injectable({ providedIn: 'root' })
export class UserService {
  private roles: string[] = [];
 
  constructor(private authService: AuthService) {}
 
  getRoles(): string[] {
    return this.roles;
  }
 
  loadRoles() {
    const userData = this.authService.getUserData();
    if (!userData) return;
 
    // Extract roles from realm_access
    const realmAccess = userData.realm_access;
    if (realmAccess && realmAccess.roles) {
      this.roles = realmAccess.roles;
    }
    
    // Extract roles from resource_access (if needed)
    const resourceAccess = userData.resource_access;
    if (resourceAccess && resourceAccess['my-client']) {
       this.roles = [...this.roles, ...resourceAccess['my-client'].roles];
    }
  }
}

You can then use this UserService in your route guards to control access.

// src/app/admin.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { UserService } from './user.service';
 
@Injectable({ providedIn: 'root' })
export class AdminGuard implements CanActivate {
  constructor(private userService: UserService, private router: Router) {}
 
  canActivate(): boolean {
    const roles = this.userService.getRoles();
    if (roles.includes('admin')) {
      return true;
    }
    this.router.navigate(['/home']);
    return false;
  }
}

Conclusion

Implementing authentication in Angular is not about copying a configuration block. It is about understanding the state machine: the redirect, the code exchange, the token storage, and the claim extraction. The angular-oauth2-oidc library abstracts the HTTP protocol details, allowing you to focus on the application logic—managing the user's session state and enforcing access control based on the claims returned by the identity provider.

By explicitly handling the PKCE flow, validating the storage strategy, and mapping IdP-specific claims like Keycloak's realm_access to your application's roles, you build a security posture that is robust against common SPA attacks like CSRF and token leakage. The library is a tool, but the architecture you build around it defines the security of your application.

Common Pitfalls

  1. Relying on LocalStorage for Tokens: Using localStorage leaves tokens exposed to XSS attacks; always prefer sessionStorage or HttpOnly cookies for sensitive data.
  2. Ignoring Discovery Document Validation: Disabling strictDiscoveryDocumentValidation in production can leave your app vulnerable to man-in-the-middle attacks that spoof the IdP configuration.
  3. Assuming Automatic Role Mapping: The library does not automatically map Keycloak-specific claims like realm_access to application roles; you must implement custom parsing logic to extract these values.

Practical Takeaways

  1. Principle of Least Privilege: Configure your OIDC scopes to request only the minimum necessary data (e.g., openid, profile) rather than broad access.
  2. Defense in Depth: Never rely solely on client-side validation; always validate tokens and signatures server-side before processing sensitive requests.
  3. Stateless Security: Treat the browser as an untrusted environment; ensure that session management relies on secure, server-controlled mechanisms like HttpOnly cookies where possible.

FAQ

Q: Does angular-oauth2-oidc support refresh tokens? A: Yes, the library supports refresh tokens, but they must be explicitly enabled in the configuration and supported by the Identity Provider.

Q: Can I use angular-oauth2-oidc with providers other than Keycloak? A: Yes, the library is designed to be provider-agnostic and works with any OIDC-compliant Identity Provider, including Auth0, Azure AD, and Okta.

Q: How do I handle token expiration if the user is on a background tab? A: The library provides setupAutomaticSilentRefresh() which attempts to refresh the token silently in the background. If the refresh fails (e.g., due to a revoked session), it triggers the login flow when the user returns to the tab.

Related posts