OAuth 2.0, OpenID Connect & JWT Security Architecture Guide
The comprehensive production guide to modern web authentication: OAuth 2.0 Authorization Code Flow with PKCE, JWT token anatomy, secure storage preventing XSS, signature verification (RS256 vs HS256), and refresh token rotation.
1. The Mental Model: OAuth 2.0 vs OpenID Connect (OIDC)
| Concept | OAuth 2.0 | OpenID Connect (OIDC) | Copy |
|---|---|---|---|
| Primary Question | "What is this client allowed to access?" | "Who is the authenticated user?" | |
| Artifact Issued | Access Token (opaque or JWT) with scopes | ID Token (signed JWT) with identity claims | |
| Consumer | Resource Server (API backend) | Client Application (Frontend / Native App) | |
| User Profile Endpoint | Vendor-specific (non-standard) | Standardized /userinfo endpoint |
2. Authorization Code Flow with PKCE (Proof Key for Code Exchange)
// 1. Generate high-entropy random code_verifier (43-128 chars)
const verifier = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map(b => b.toString(16).padStart(2, "0")).join("");
// 2. Hash verifier with SHA-256 and Base64URL encode to create code_challenge
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
// 3. Initiate Auth Request:
// GET /authorize?response_type=code&client_id=APP_ID&code_challenge=CHALLENGE&code_challenge_method=S256The server verifies that SHA-256(verifier) matches the previously submitted code_challenge before issuing tokens.
3. JWT Token Anatomy & Standard Claims
{
"iss": "https://auth.example.com/", // Issuer: who created the token
"sub": "usr_94820a1b", // Subject: unique user identifier
"aud": "https://api.example.com", // Audience: intended recipient backend
"exp": 1792765200, // Expiration Time (Unix epoch seconds)
"nbf": 1792761600, // Not Before: token invalid before this date
"iat": 1792761600, // Issued At: generation timestamp
"jti": "d3b07384-d113-467f-8d2a-...", // JWT ID: unique identifier for revocation
"roles": ["editor", "billing_admin"] // Custom authorization claims
}APIs must validate iss, aud, exp, and nbf on every request before trusting the payload content.
4. Token Storage: Defeating XSS & CSRF Attacks
/* RECOMMENDED ARCHITECTURE:
* 1. Short-lived Access Token (5-15 min): Stored in JavaScript memory (closure/state).
* 2. Long-lived Refresh Token (7-30 days): Stored in an HttpOnly, Secure, SameSite=Strict cookie.
*/
// Backend Cookie Configuration Header
Set-Cookie: __Host-refresh_token=rt_9fa81bc3; \
Path=/api/auth/refresh; \
HttpOnly; \
Secure; \
SameSite=Strict; \
Max-Age=2592000HttpOnly prevents JavaScript from reading the cookie; __Host- prefix guarantees Secure delivery and prevents subdomain spoofing.
5. Refresh Token Rotation & Reuse Detection
// When /api/auth/refresh receives a token:
async function handleRefresh(incomingToken) {
const tokenRecord = await db.findToken(incomingToken);
// REUSE DETECTION: If this token was already consumed, a breach has occurred!
if (tokenRecord.consumed) {
await db.revokeAllTokensForUser(tokenRecord.userId);
throw new SecurityException("Breach detected: Revoking all active user sessions!");
}
// Mark current token as consumed
await db.markConsumed(incomingToken);
// Issue new access token + new rotated refresh token
const newAccessToken = generateJWT(tokenRecord.userId, "15m");
const newRefreshToken = generateSecureOpaqueToken();
await db.saveNewTokenInFamily(newRefreshToken, tokenRecord.familyId);
return { newAccessToken, newRefreshToken };
}Automatic reuse detection renders compromised refresh tokens useless after a single use.
Related Free Online Tools
Put these techniques into practice immediately with our privacy-first browser utilities:
Decode and inspect JSON Web Token headers, payloads, and expiration timestamps locally in your browser.
Generate cryptographic HMAC-SHA256 signatures to verify webhook and token integrity.
Inspect Base64 and Base64URL encoded token headers and claims.