←Back to Guides
๐Ÿ”Security & Authโฑ๏ธ 16 min read•Updated October 2026

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.

#OAuth2#JWT#Security#Authentication#Web Development#API#Cryptography

1. The Mental Model: OAuth 2.0 vs OpenID Connect (OIDC)

OAuth 2.0 is an authorization framework designed to grant access to resources without sharing credentials (delegated access). OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that provides authentication (verifying who the user is).
ConceptOAuth 2.0OpenID Connect (OIDC)Copy
Primary Question"What is this client allowed to access?""Who is the authenticated user?"
Artifact IssuedAccess Token (opaque or JWT) with scopesID Token (signed JWT) with identity claims
ConsumerResource Server (API backend)Client Application (Frontend / Native App)
User Profile EndpointVendor-specific (non-standard)Standardized /userinfo endpoint

2. Authorization Code Flow with PKCE (Proof Key for Code Exchange)

The legacy Implicit Flow is officially deprecated due to token leakage in browser history and referrer headers. All modern web, mobile, and Single Page Applications (SPAs) must use Authorization Code Flow with PKCE to prevent authorization code interception attacks.
Generating PKCE Code Verifier & Challenge
// 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=S256

The server verifies that SHA-256(verifier) matches the previously submitted code_challenge before issuing tokens.

3. JWT Token Anatomy & Standard Claims

A JSON Web Token consists of three Base64URL-encoded parts separated by dots: Header.Payload.Signature. The header defines the cryptographic algorithm (e.g. RS256, HS256), the payload contains standardized and custom claims, and the signature guarantees tamper resistance.
Essential Standard JWT 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

Storing JWT access tokens in localStorage or sessionStorage leaves them vulnerable to Cross-Site Scripting (XSS): any rogue third-party npm package, analytics script, or compromised dependency can read and exfiltrate your users tokens.
Hardened Token Storage Architecture
/* 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=2592000

HttpOnly prevents JavaScript from reading the cookie; __Host- prefix guarantees Secure delivery and prevents subdomain spoofing.

5. Refresh Token Rotation & Reuse Detection

Refresh Token Rotation (RTR) invalidates the old refresh token every single time a new access token is requested, issuing a brand-new refresh token in response. If a stolen refresh token is used twice, the auth server immediately revokes all tokens in the entire family session.
Token Rotation Family State Machine
// 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.

๐ŸŽ“Explore More Guides

โœจ