| |

Angular 118 🅰️ Authentication with OAuth and JWT

⏱️ Estimated read: 16 min | |

Authentication in Angular is not a single feature but a layered system: the OAuth 2.0 and OpenID Connect protocols handle the exchange of credentials for tokens, JWT (JSON Web Token) provides the token format, route guards control client-side navigation, and HTTP interceptors attach tokens to API requests. Each layer has a specific responsibility, and each has pitfalls that surface when the layer is misunderstood. The most common mistake is treating a route guard as a security control. Angular’s documentation is explicit: “Never rely on client-side guards as the sole source of access control. All JavaScript that runs in a web browser can be modified by the user running the browser. Always enforce user authorization server-side” .

The OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange) is the recommended flow for single-page applications. The implicit flow, once popular for SPAs, is now discouraged because tokens are exposed in URL fragments and browser history . The authorization code flow with PKCE keeps tokens out of the URL, uses a dynamically generated code verifier that only the originating client can present, and supports refresh tokens for silent renewal . For an Angular application, this means redirecting the user to the identity provider, receiving an authorization code at a callback URL, and exchanging that code for tokens through a backend or a public client exchange.

This chapter covers the OAuth 2.0 and OIDC protocol roles, the JWT structure and validation rules, the authorization code flow with PKCE, route guards and HTTP interceptors, token storage and refresh strategies, and the security practices that prevent common authentication vulnerabilities.

Key point: OAuth 2.0 delegates authorization; OIDC adds authentication on top. JWTs are the token format. The authorization code flow with PKCE is the recommended flow for SPAs. Route guards hide UI but do not secure endpoints; server-side authorization is mandatory. HTTP interceptors attach tokens automatically, but token storage must balance security against usability.


Why OAuth and JWT exist

The password-sharing problem. Before OAuth, an application that needed to access a user’s data on another service had to ask for the user’s password. The application then had full access to the user’s account, and the user had no way to revoke that access without changing the password. OAuth 2.0 solves this by introducing an authorization server that issues tokens with specific scopes and lifetimes. The application never sees the password, and the user can revoke access at any time .

The authentication-versus-authorization problem. OAuth 2.0 is an authorization framework, not an authentication protocol. It grants access to resources but does not tell the application who the user is. OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0. The authorization server issues an ID token—a JWT containing claims about the user—alongside the access token. The client uses the ID token to authenticate the user, and the access token to call APIs .

The token-format problem. Tokens need a standard format that both the issuer and the consumer can parse and validate. JWT provides this: a compact, URL-safe format with a header, payload, and signature. The signature allows the consumer to verify that the token was issued by the expected authority and has not been tampered with. The payload carries claims about the user and the token’s validity .

The client-side-storage problem. Tokens must be stored somewhere. localStorage is persistent but accessible to any JavaScript on the page, making it vulnerable to XSS. In-memory storage is safer but lost on page refresh. httpOnly cookies are not accessible to JavaScript but require CSRF protection. Each option has trade-offs, and the choice depends on the application’s threat model .

The refresh problem. Access tokens are short-lived for security reasons. When they expire, the user should not have to log in again. Refresh tokens solve this: a long-lived credential that the client exchanges for a new access token without user interaction. The refresh token must be stored securely because it grants the ability to obtain new access tokens .


a. OAuth 2.0 and OIDC protocol roles

The OAuth 2.0 authorization framework defines four roles :

Authorization server — The identity provider (IdP) that authenticates the user and issues tokens. Examples include Microsoft Entra ID, Google Identity, Okta, Auth0, and Keycloak. It handles the user’s credentials, consent, and token issuance .

Client — The application requesting access. In the Angular context, the client is the SPA running in the browser. It initiates the authorization flow and uses the tokens to call APIs .

Resource owner — The end user who owns the protected data. The user grants or denies the client’s request to access their data .

Resource server — The API that hosts the protected resources. It validates the access token and responds to requests. The resource server relies on the authorization server for authentication .

OIDC extends this with an ID token, which contains claims about the authenticated user. The ID token is a JWT, and its sub claim is the user’s unique identifier, iss is the issuer, aud is the client ID, and exp is the expiration time .


b. JWT structure and validation

A JWT has three base64url-encoded parts separated by dots: header, payload, and signature. The header declares the signing algorithm and token type. The payload contains claims. The signature verifies integrity .

Validation rules are critical and often misunderstood. The JWT Best Current Practices document specifies that implementations must confirm the JWT is in legal format, reject tokens with unexpected characters, and use mutually exclusive validation rules for different kinds of JWTs . Explicit typing via the typ header is recommended for new JWT applications to prevent cross-JWT confusion .

For OIDC, the ID token validation must check the issuer, audience, expiration, and signature against the provider’s JWKS (JSON Web Key Set). The ngx-oauth library performs this validation using the jose library when strictJwt: true is set .

A critical warning: JWT payloads are base64url-encoded, not encrypted. Anyone can decode the payload. Sensitive data must never be stored in JWT claims. The signature prevents tampering but does not provide confidentiality .


c. The authorization code flow with PKCE

The authorization code flow with PKCE is the recommended flow for SPAs. The steps are :

  1. The client generates a random code_verifier and derives a code_challenge from it.
  2. The client redirects the user to the authorization server’s /authorize endpoint with the code_challenge, client_id, redirect_uri, and scope.
  3. The user authenticates and consents.
  4. The authorization server redirects back to the redirect_uri with an authorization code.
  5. The client exchanges the code for tokens at the /token endpoint, sending the code_verifier to prove it originated the request.
  6. The authorization server validates the code_verifier against the stored code_challenge and issues tokens.

PKCE prevents authorization code interception attacks because an attacker who intercepts the code cannot exchange it without the code_verifier .

The angular-auth-oidc-client library implements this flow and is certified by the OpenID Foundation . Configuration includes the authority URL, client ID, redirect URL, and scopes . The checkAuth() method must be called on application startup to process the redirect from the identity provider .


d. Route guards and HTTP interceptors

Angular’s functional route guards control navigation. The CanActivateFn checks whether the user can access a route and returns a boolean, a UrlTree for redirect, or a Promise/Observable of either .

export const authGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  return authService.isAuthenticated();
};

The guard is applied to routes in the array form: canActivate: [authGuard] .

The critical caveat is repeated in every Angular authentication library’s documentation: guards are for user experience, not security. MSAL Guard’s documentation states: “MsalGuard is a convenience class you can use to improve the user experience, but it should not be relied upon for security. Attackers can potentially get around client-side guards, and you should ensure that the server does not return any data the user should not access” .

HTTP interceptors attach the access token to outgoing requests. The functional interceptor clones the request and sets the Authorization header .

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
  }
  return next(req);
};

The interceptor can also handle token refresh: when a 401 response is received, it attempts to refresh the token, queues concurrent requests during the refresh, and retries the failed requests with the new token .


e. Token storage and refresh strategies

Token storage is a security trade-off. The options are :

StoragePersistenceXSS RiskCSRF Risk
localStorageAcross sessionsHigh (readable by JS)Low
sessionStoragePer tabHighLow
In-memoryUntil refreshLowLow
httpOnly cookieConfigurableLow (not readable by JS)High (needs CSRF token)

The Pluralsight lab notes that localStorage is acceptable for demos but httpOnly cookies are recommended for production . The refresh token, if used, must be stored more carefully than the access token because it can obtain new access tokens .

The silent refresh strategy uses a hidden iframe or a refresh token to obtain new access tokens before expiration. The angular-auth-oidc-client supports silentRenew: true with useRefreshToken: true in its configuration . The interceptor-based approach detects 401 responses and refreshes reactively, which is simpler but means the first request after expiration fails .


f. Security practices

Do not store client secrets in the SPA. Public clients cannot keep secrets. The ngx-oauth documentation states: “A secret in browser code is not confidential and buys no security” . Use PKCE instead of a client secret.

Validate the ID token. The ID token must be validated against the issuer’s JWKS, and the iss, aud, exp, and nonce claims must be checked. Libraries like angular-auth-oidc-client and ngx-oauth perform this validation .

Use the state parameter. The state parameter prevents CSRF in the authorization flow. Google’s documentation recommends generating a high-quality random string and validating it against the stored value after the redirect .

Enforce server-side authorization. Every API endpoint must validate the access token and check the user’s permissions. The client-side guard does not protect the endpoint .

Use explicit typing for JWTs. The JWT BCP recommends the typ header to distinguish different kinds of JWTs and prevent substitution attacks .

Limit token lifetime. Access tokens should be short-lived (minutes to an hour). Refresh tokens can be longer but must be revocable .


Complete Example Session

// ============================================
// PART 1: CONFIGURE OIDC WITH ANGULAR-AUTH-OIDC-CLIENT
// ============================================
import { AuthModule, LogLevel } from 'angular-auth-oidc-client';

AuthModule.forRoot({
  config: {
    authority: 'https://your-idp.com/realms/realm',
    redirectUrl: window.location.origin,
    postLogoutRedirectUri: window.location.origin,
    clientId: 'your-client-id',
    scope: 'openid profile email offline_access',
    responseType: 'code',
    silentRenew: true,
    useRefreshToken: true,
    logLevel: LogLevel.Debug,
  },
})
// ============================================
// PART 2: CHECK AUTH ON STARTUP
// ============================================
import { Component, OnInit, inject } from '@angular/core';
import { OidcSecurityService } from 'angular-auth-oidc-client';

@Component({ /* ... */ })
export class AppComponent implements OnInit {
  private readonly oidcSecurityService = inject(OidcSecurityService);

  ngOnInit() {
    this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData }) => {
      // isAuthenticated: boolean
      // userData: claims from ID token
    });
  }
}
// ============================================
// PART 3: LOGIN AND LOGOUT
// ============================================
login() {
  this.oidcSecurityService.authorize();
}

logout() {
  this.oidcSecurityService.logoff().subscribe();
}
// ============================================
// PART 4: AUTH GUARD
// ============================================
import { CanActivateFn } from '@angular/router';
import { inject } from '@angular/core';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { map, take } from 'rxjs';

export const authGuard: CanActivateFn = () => {
  const oidc = inject(OidcSecurityService);
  return oidc.isAuthenticated$.pipe(
    take(1),
    map(isAuth => isAuth)
  );
};
// ============================================
// PART 5: HTTP INTERCEPTOR FOR TOKEN
// ============================================
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { OidcSecurityService } from 'angular-auth-oidc-client';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const oidc = inject(OidcSecurityService);

  return oidc.getAccessToken().pipe(
    take(1),
    switchMap(token => {
      if (token) {
        req = req.clone({
          setHeaders: { Authorization: `Bearer ${token}` }
        });
      }
      return next(req);
    })
  );
};
// ============================================
// PART 6: REGISTER INTERCEPTOR
// ============================================
import { provideHttpClient, withInterceptors } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(withInterceptors([authInterceptor])),
  ],
};
// ============================================
// PART 7: ROUTES WITH GUARD
// ============================================
const routes: Routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard],
  },
];
// ============================================
// PART 8: JWT DECODE AND CLAIMS
// ============================================
import { jwtDecode } from 'jwt-decode';

interface JwtPayload {
  sub: string;
  email: string;
  roles: string[];
  exp: number;
}

const decoded = jwtDecode<JwtPayload>(token);
const isExpired = decoded.exp * 1000 < Date.now();
// ============================================
// PART 9: SERVER-SIDE VALIDATION (CONCEPTUAL)
// ============================================
// The server must validate:
// - Signature against JWKS
// - iss (issuer)
// - aud (audience)
// - exp (expiration)
// - Permissions for the requested resource
// ============================================
// PART 10: SECURITY CHECKLIST
// ============================================
// - PKCE for authorization code flow
// - No client secret in SPA
// - Validate ID token claims
// - Use state parameter for CSRF
// - Short access token lifetime
// - Refresh token storage secure
// - Server-side authorization enforced
// - Logout clears all state

These ten parts cover OIDC configuration, startup authentication check, login/logout, route guard, HTTP interceptor, interceptor registration, route configuration, JWT decoding, server-side validation, and a security checklist.


Quick Reference

OAuth 2.0 / OIDC Roles

RoleResponsibility
Authorization serverAuthenticates user, issues tokens
ClientRequests access; the Angular SPA
Resource ownerThe end user
Resource serverAPI that validates tokens

Token Types

TokenPurposeContains
Access tokenCall APIsScopes, permissions
ID tokenAuthenticate userUser claims (sub, email)
Refresh tokenObtain new tokensLong-lived credential

JWT Validation

ClaimValidation
issMatches expected issuer
audMatches client ID
expNot expired
subUser identifier
SignatureVerified against JWKS

Storage Trade-offs

StorageXSS RiskCSRF RiskPersistence
localStorageHighLowYes
sessionStorageHighLowPer tab
In-memoryLowLowNo
httpOnly cookieLowHighConfigurable

Angular Auth Libraries

LibraryNotes
angular-auth-oidc-clientOIDC certified, PKCE, refresh
ngx-oauthAngular 21+, zoneless, signals
angular-oauth2-oidcOlder, widely used
MSAL AngularMicrosoft Entra ID

Route Guards

GuardPurpose
CanActivateCan user access route
CanActivateChildCan user access child routes
CanDeactivateCan user leave route
CanMatchConditional route matching

Best Practices

✅ Do This:

// Use PKCE for authorization code flow
pkce: true

// Validate ID token
strictJwt: true

// Server-side authorization
// Every API endpoint checks the token and permissions

// Short access token lifetime
// Access tokens expire in minutes

// Use httpOnly cookies for production
// with CSRF protection

❌ Don’t Do This:

// Client secret in SPA
clientSecret: 'secret'  // ❌ Not confidential

// Implicit flow
responseType: 'token'  // ❌ Deprecated

// Rely on guard alone
canActivate: [authGuard]  // ❌ Server must also check

// Store sensitive data in JWT
// JWT payload is base64url, not encrypted

Common Pitfalls

PitfallWhy It HappensFix
Token not attachedInterceptor not registeredprovideHttpClient(withInterceptors(...))
Redirect loopGuard redirects to login, login redirects to protectedCheck auth state before redirect
Token expiredNo refresh strategyUse refresh tokens or silent renew
XSS steals tokenlocalStorage usedUse httpOnly cookies
CSRF on cookie authNo CSRF tokenUse state parameter and CSRF headers
ID token not validatedNo JWKS checkEnable strictJwt or library validation
Client secret exposedStored in SPARemove; use PKCE
Guard bypassedClient-side onlyEnforce server-side authorization

Real-World Examples

1. OIDC Configuration

AuthModule.forRoot({
  config: {
    authority: 'https://idp.example.com',
    clientId: 'spa-client',
    scope: 'openid profile email',
    responseType: 'code',
  },
})

2. Auth Guard

export const authGuard: CanActivateFn = () => {
  return inject(OidcSecurityService).isAuthenticated$;
};

3. HTTP Interceptor

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
  }
  return next(req);
};

4. Login

this.oidcSecurityService.authorize();

5. Logout

this.oidcSecurityService.logoff().subscribe();

6. Get Access Token

const token = this.oidcSecurityService.getAccessToken();

7. Decode JWT

const claims = jwtDecode<JwtPayload>(token);

8. Check Expiration

const isExpired = claims.exp * 1000 < Date.now();

9. Refresh Token

this.oidcSecurityService.forceRefreshSession().subscribe();

10. Server-Side Check (ASP.NET)

[Authorize]
public IActionResult GetData() { }

Visual

OAuth 2.0 Authorization Code Flow with PKCE

┌──────────────────────────────────────────────────────────────┐
│  USER         CLIENT (SPA)      AUTH SERVER      RESOURCE    │
│  │               │                  │              SERVER    │
│  │               │                  │              │         │
│  │  Click login  │                  │              │         │
│  │─────────────▶│                  │              │         │
│  │               │ Generate         │              │         │
│  │               │ code_verifier    │              │         │
│  │               │ and challenge    │              │         │
│  │               │                  │              │         │
│  │  Redirect to /authorize ───────▶│              │         │
│  │               │                  │              │         │
│  │  Authenticate │                  │              │         │
│  │───────────────┼────────────────▶│              │         │
│  │               │                  │              │         │
│  │  Redirect with code ◀───────────│              │         │
│  │               │                  │              │         │
│  │               │ POST /token ───▶│              │         │
│  │               │ (code + verifier)│              │         │
│  │               │                  │              │         │
│  │               │ Access + ID token│              │         │
│  │               │◀────────────────│              │         │
│  │               │                  │              │         │
│  │               │ API request with token ───────▶│         │
│  │               │                  │              │         │
│  │               │ Validate token   │              │         │
│  │               │────────────────▶│              │         │
│  │               │                  │              │         │
│  │               │ Response ◀─────────────────────│         │
└──────────────────────────────────────────────────────────────┘

Token Types and Their Roles

┌──────────────────────────────────────────────────────────────┐
│  ACCESS TOKEN                     ID TOKEN                   │
│  ┌─────────────────────┐         ┌─────────────────────┐    │
│  │  Purpose: Call APIs │         │  Purpose: User info │    │
│  │  Contains: Scopes   │         │  Contains: Claims   │    │
│  │  Lifetime: Short    │         │  Lifetime: Short    │    │
│  │  Validation: API    │         │  Validation: Client │    │
│  └─────────────────────┘         └─────────────────────┘    │
│                                                              │
│  REFRESH TOKEN                                               │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  Purpose: Obtain new access tokens                     │  │
│  │  Contains: Credential to refresh                       │  │
│  │  Lifetime: Long                                        │  │
│  │  Validation: Auth server                               │  │
│  │  Storage: Must be secure                               │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Client-Side vs Server-Side Security

┌──────────────────────────────────────────────────────────────┐
│  CLIENT-SIDE (Angular)          │  SERVER-SIDE (API)        │
│  ───────────────────────────────┼────────────────────────── │
│  Route guards                   │  Token validation         │
│  - Control navigation           │  - Signature check        │
│  - Can be bypassed              │  - Issuer check           │
│                                 │  - Audience check         │
│  HTTP interceptors              │  - Expiration check       │
│  - Attach token                 │                           │
│  - Can be modified              │  Authorization            │
│                                 │  - Role/permission check  │
│  NEVER the sole control         │  ALWAYS enforced          │
└──────────────────────────────────────────────────────────────┘

Token Storage Trade-offs

┌──────────────────────────────────────────────────────────────┐
│  localStorage                    httpOnly COOKIE             │
│  ┌─────────────────────┐         ┌─────────────────────┐    │
│  │  Readable by JS     │         │  Not readable by JS │    │
│  │  XSS steals token   │         │  XSS cannot steal   │    │
│  │  No CSRF risk       │         │  CSRF risk          │    │
│  │  Persistent         │         │  Configurable       │    │
│  └─────────────────────┘         └─────────────────────┘    │
│                                                              │
│  In-memory                                                   │
│  ┌─────────────────────┐                                     │
│  │  Not persistent     │                                     │
│  │  Lost on refresh    │                                     │
│  │  Safest for XSS     │                                     │
│  └─────────────────────┘                                     │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
OAuth 2.0Authorization framework
OIDCAuthentication layer on OAuth 2.0
JWTToken format: header, payload, signature
Recommended flowAuthorization Code with PKCE
PKCEPrevents code interception
ID tokenAuthenticates user; contains claims
Access tokenCalls APIs; contains scopes
Refresh tokenObtains new access tokens
Route guardsClient-side navigation control
HTTP interceptorAttaches token to requests
Server-side authorizationMandatory for endpoint protection
Token storagehttpOnly cookies for production

Key takeaways:

  • OAuth 2.0 delegates authorization; OIDC adds authentication. OAuth grants access to resources through tokens. OIDC extends it with an ID token that identifies the user. The authorization server is the identity provider; the client is the SPA; the resource server is the API .
  • The authorization code flow with PKCE is the recommended flow for SPAs. The implicit flow exposes tokens in URLs and is discouraged. PKCE prevents authorization code interception because the attacker cannot exchange the code without the code verifier .
  • JWT is the token format, not an authentication protocol. JWTs are base64url-encoded, not encrypted. Sensitive data must never be placed in claims. Validation must check the issuer, audience, expiration, and signature against the provider’s JWKS .
  • Route guards control navigation but do not secure endpoints. Angular’s documentation and every authentication library repeat this: client-side guards can be bypassed. Server-side authorization is mandatory for every protected API .
  • HTTP interceptors attach tokens to requests automatically. A functional interceptor clones the request and sets the Authorization header. It can also handle 401 responses and token refresh, queuing concurrent requests during refresh .
  • Token storage is a trade-off. localStorage is vulnerable to XSS. httpOnly cookies are not readable by JavaScript but require CSRF protection. In-memory storage is safest against XSS but lost on refresh. Production applications should use httpOnly cookies .
  • Never store a client secret in the SPA. Public clients cannot keep secrets. Use PKCE instead. A secret in browser code “buys no security” .
  • Use the state parameter to prevent CSRF. The authorization request should include a random state value, and the callback should validate it against the stored value .

Remember: Authentication in Angular is a chain of trust that begins at the identity provider and ends at the resource server. The OIDC library handles the authorization code flow with PKCE, validates the ID token, and manages token refresh. The route guard controls which pages the user can navigate to. The HTTP interceptor attaches the access token to API requests. But none of these client-side mechanisms is a security control by itself. The route guard is a convenience; the server must enforce authorization. The interceptor is a convenience; the server must validate the token. The library is a convenience; the developer must understand what it does and does not protect against. When the chain is complete—PKCE for the flow, validated tokens, server-side authorization, secure storage, and short token lifetimes—the application is resistant to the common authentication attacks. When any link is missing, the application is vulnerable despite the framework’s protections.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!