e Solution
The production-ready approach for modern web and mobile clients is the OAuth2 Authorization Code flow enhanced with PKCE, wrapped by OIDC for identity resolution. This architecture separates authentication (OIDC) from resource authorization (OAuth2 scopes), enforces cryptographic proof of possession, and eliminates authorization code interception attacks.
Architecture Decisions and Rationale
- Flow Selection: Authorization Code + PKCE is mandatory for public clients (SPAs, mobile apps). Confidential clients (backend services) may use standard Authorization Code with client_secret, but PKCE is increasingly recommended across all client types to future-proof against secret leakage.
- Token Handling: ID tokens are validated server-side or in a secure execution context. Access tokens are never stored in browser-accessible storage. Session state is managed via HTTP-only, Secure cookies to mitigate XSS.
- Validation Strategy: ID tokens are validated against the issuer's JWKS endpoint. Claims (
iss, aud, exp, nonce) are strictly enforced. Signature verification uses RS256/ES256 algorithms only; none and symmetric algorithms are rejected.
- State Management:
state prevents CSRF during the authorization redirect. nonce binds the ID token to the specific authentication request. Both are cryptographically random and single-use.
Step-by-Step Implementation (TypeScript)
Step 1: PKCE Generator
import { randomBytes, createHash } from 'crypto';
export function generatePKCE() {
const codeVerifier = randomBytes(32).toString('base64url');
const codeChallenge = createHash('sha256')
.update(codeVerifier)
.digest('base64url');
return { codeVerifier, codeChallenge };
}
Step 2: Authorization Request Construction
export function buildAuthorizationUrl(
issuer: string,
clientId: string,
redirectUri: string,
codeChallenge: string,
state: string,
nonce: string
): string {
const authEndpoint = `${issuer}/authorize`;
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state,
nonce,
});
return `${authEndpoint}?${params.toString()}`;
}
Step 3: Token Exchange
export async function exchangeCodeForTokens(
issuer: string,
clientId: string,
codeVerifier: string,
code: string,
redirectUri: string,
clientSecret?: string
) {
const tokenEndpoint = `${issuer}/token`;
const body = new URLSearchParams({
grant_type: 'authorization_code',
code,
redirect_uri: redirectUri,
client_id: clientId,
code_verifier: codeVerifier,
});
if (clientSecret) body.append('client_secret', clientSecret);
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!response.ok) throw new Error(`Token exchange failed: ${response.status}`);
return response.json();
}
Step 4: ID Token Validation
import * as jose from 'jose';
export async function validateIdToken(
idToken: string,
issuer: string,
clientId: string,
expectedNonce: string
) {
const JWKS = jose.createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`));
const { payload } = await jose.jwtVerify(idToken, JWKS, {
issuer,
audience: clientId,
algorithms: ['RS256', 'ES256'],
});
if (payload.nonce !== expectedNonce) {
throw new Error('Nonce mismatch: potential replay attack');
}
if (typeof payload.exp !== 'number' || payload.exp * 1000 < Date.now()) {
throw new Error('ID token expired');
}
return payload;
}
Architecture Rationale
The implementation isolates cryptographic operations, enforces strict claim validation, and leverages standard JWKS resolution. By decoupling token exchange from validation, the system supports horizontal scaling and cache-friendly JWKS fetching. The use of base64url encoding for PKCE aligns with RFC 7636, and the rejection of none/HS256 prevents algorithm confusion attacks. Session management should route validated claims through a secure cookie middleware, never exposing raw tokens to the client runtime.
Pitfall Guide
-
Using OAuth2 Scopes for Authentication
OAuth2 scopes (read:users, write:data) govern resource access, not identity verification. Implementing authentication by checking scope presence allows token reuse attacks and bypasses identity binding. Always require openid scope and validate the ID token's sub claim.
-
Storing Tokens in localStorage or sessionStorage
Browser storage is accessible to all JavaScript on the origin. XSS payloads can exfiltrate tokens silently. Use HTTP-only, Secure, SameSite cookies for session tokens. If client-side access is unavoidable, limit storage to short-lived access tokens and rotate them aggressively.
-
Omitting state or nonce Parameters
Missing state enables CSRF during the authorization redirect. Missing nonce allows ID token replay attacks where an attacker reuses a previously issued token. Generate cryptographically random values per request and validate them strictly during token exchange.
-
Skipping PKCE for Public Clients
Authorization code interception is trivial on mobile and SPA environments without PKCE. The code_verifier proves the token requester is the original authorizer. Omitting PKCE reduces security to legacy implicit flow levels, which are deprecated for good reason.
-
Blindly Trusting the iss Claim Without JWKS Validation
The iss claim is user-controllable in the JWT header. Validation must verify the signature against the issuer's published JWKS and confirm the iss matches the expected provider URL. Skipping JWKS resolution enables token forgery from malicious identity providers.
-
Improper Redirect URI Validation
Allowing wildcard or partially matched redirect URIs enables open redirect attacks that steal authorization codes. The authorization server must perform exact string matching against pre-registered URIs. Client-side validation should never be the sole control.
-
Ignoring Token Refresh Mechanics
Access tokens are short-lived by design. Failing to implement secure refresh token rotation leads to session drops or forces developers to extend token lifetimes, increasing the window for token theft. Use refresh tokens with rotation and revocation tracking. Store them server-side or in secure storage, never in client memory.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single Page Application | OIDC Code + PKCE with HTTP-only cookie session | Eliminates XSS token exposure, meets modern browser security defaults | Low (standard library support) |
| Native Mobile App | OIDC Code + PKCE with App Link/Universal Link redirect | Prevents code interception, leverages OS-level redirect security | Medium (requires deep link configuration) |
| B2B API Gateway | OAuth2 Client Credentials + OIDC for admin portal | Separates machine auth from human auth; reduces token surface | Low (standardized flow) |
| Legacy Monolith Migration | OIDC Code + PKCE with backend-for-frontend (BFF) pattern | Isolates token handling, enables gradual frontend decoupling | High (architectural refactor) |
Configuration Template
// oidc.config.ts
export const OIDC_CONFIG = {
issuer: process.env.OIDC_ISSUER!,
clientId: process.env.OIDC_CLIENT_ID!,
redirectUri: process.env.OIDC_REDIRECT_URI!,
scopes: ['openid', 'profile', 'email'],
algorithms: ['RS256', 'ES256'] as const,
tokenEndpoint: '/token',
authorizationEndpoint: '/authorize',
jwksEndpoint: '/.well-known/jwks.json',
cookie: {
name: '__session',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
maxAge: 3600,
},
pkce: {
enabled: true,
method: 'S256' as const,
},
validation: {
requireNonce: true,
requireState: true,
clockTolerance: 30,
},
};
Quick Start Guide
- Register your application in the identity provider console; record
client_id, issuer, and exact redirect_uri.
- Install dependencies:
npm install jose @badgateway/oauth2-client (or equivalent standard libraries).
- Implement PKCE generation, authorization URL construction, and token exchange using the provided TypeScript templates.
- Add JWKS-based ID token validation with strict claim checking; route successful validation to an HTTP-only cookie session.
- Test the flow with provider sandbox credentials; verify
state, nonce, and PKCE enforcement via browser dev tools and token introspection.