Angular 118 🅰️ Authentication with OAuth and JWT
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 :
- The client generates a random
code_verifierand derives acode_challengefrom it. - The client redirects the user to the authorization server’s
/authorizeendpoint with thecode_challenge,client_id,redirect_uri, andscope. - The user authenticates and consents.
- The authorization server redirects back to the
redirect_uriwith an authorizationcode. - The client exchanges the code for tokens at the
/tokenendpoint, sending thecode_verifierto prove it originated the request. - The authorization server validates the
code_verifieragainst the storedcode_challengeand 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 :
| Storage | Persistence | XSS Risk | CSRF Risk |
|---|---|---|---|
localStorage | Across sessions | High (readable by JS) | Low |
sessionStorage | Per tab | High | Low |
| In-memory | Until refresh | Low | Low |
httpOnly cookie | Configurable | Low (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
| Role | Responsibility |
|---|---|
| Authorization server | Authenticates user, issues tokens |
| Client | Requests access; the Angular SPA |
| Resource owner | The end user |
| Resource server | API that validates tokens |
Token Types
| Token | Purpose | Contains |
|---|---|---|
| Access token | Call APIs | Scopes, permissions |
| ID token | Authenticate user | User claims (sub, email) |
| Refresh token | Obtain new tokens | Long-lived credential |
JWT Validation
| Claim | Validation |
|---|---|
iss | Matches expected issuer |
aud | Matches client ID |
exp | Not expired |
sub | User identifier |
| Signature | Verified against JWKS |
Storage Trade-offs
| Storage | XSS Risk | CSRF Risk | Persistence |
|---|---|---|---|
localStorage | High | Low | Yes |
sessionStorage | High | Low | Per tab |
| In-memory | Low | Low | No |
httpOnly cookie | Low | High | Configurable |
Angular Auth Libraries
| Library | Notes |
|---|---|
angular-auth-oidc-client | OIDC certified, PKCE, refresh |
ngx-oauth | Angular 21+, zoneless, signals |
angular-oauth2-oidc | Older, widely used |
MSAL Angular | Microsoft Entra ID |
Route Guards
| Guard | Purpose |
|---|---|
CanActivate | Can user access route |
CanActivateChild | Can user access child routes |
CanDeactivate | Can user leave route |
CanMatch | Conditional 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Token not attached | Interceptor not registered | provideHttpClient(withInterceptors(...)) |
| Redirect loop | Guard redirects to login, login redirects to protected | Check auth state before redirect |
| Token expired | No refresh strategy | Use refresh tokens or silent renew |
| XSS steals token | localStorage used | Use httpOnly cookies |
| CSRF on cookie auth | No CSRF token | Use state parameter and CSRF headers |
| ID token not validated | No JWKS check | Enable strictJwt or library validation |
| Client secret exposed | Stored in SPA | Remove; use PKCE |
| Guard bypassed | Client-side only | Enforce 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
| Item | Value |
|---|---|
| OAuth 2.0 | Authorization framework |
| OIDC | Authentication layer on OAuth 2.0 |
| JWT | Token format: header, payload, signature |
| Recommended flow | Authorization Code with PKCE |
| PKCE | Prevents code interception |
| ID token | Authenticates user; contains claims |
| Access token | Calls APIs; contains scopes |
| Refresh token | Obtains new access tokens |
| Route guards | Client-side navigation control |
| HTTP interceptor | Attaches token to requests |
| Server-side authorization | Mandatory for endpoint protection |
| Token storage | httpOnly 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
Authorizationheader. It can also handle 401 responses and token refresh, queuing concurrent requests during refresh . - Token storage is a trade-off.
localStorageis vulnerable to XSS.httpOnlycookies are not readable by JavaScript but require CSRF protection. In-memory storage is safest against XSS but lost on refresh. Production applications should usehttpOnlycookies . - 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.
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!