Securing web applications and microservice APIs requires robust authentication and authorization. **OAuth 2.0** (RFC 6749) is the industry-standard delegated authorization framework, while **JSON Web Tokens (JWT)** (RFC 7519) provide compact, self-contained stateless claims signed cryptographically.

However, misconfiguring OAuth flows or naively storing JWTs in browser local storage leaves applications vulnerable to authorization code interception attacks, Cross-Site Scripting (XSS), and token replay exploits. In this guide, we break down PKCE authorization flows, cryptographic signing algorithms, and Refresh Token Rotation.

1. Why OAuth 2.0 Implicit Flow is Deprecated: Enter PKCE

Historically, single-page web applications (SPAs) and mobile apps used the OAuth 2.0 Implicit Grant Flow because public clients could not securely store a client_secret. The authorization server returned access tokens directly in HTTP redirect URL fragments.

This flow was deprecated due to extreme security risks: malicious mobile apps or browser extensions could intercept authorization tokens directly from browser history or redirect logs.

Modern OAuth 2.0 security mandates **Authorization Code Flow with PKCE (Proof Key for Code Exchange - RFC 7636)** for all public and confidential clients alike.

2. How PKCE Mechanics Prevent Authorization Code Interception

PKCE dynamically generates a cryptographic key pair per authorization request:

  1. Code Verifier: Client generates a cryptographically random string $V$ (43–128 characters long).
  2. Code Challenge: Client computes the SHA-256 hash of the verifier and Base64URL encodes it: $C = \text{Base64URL}(\text{SHA256}(V))$.
  3. Authorization Request: Client sends $C$ to the authorization server along with the authorization request. The server logs $C$.
  4. Token Exchange: Upon receiving the authorization code, the client sends the raw $V$ to the /oauth/token endpoint. The server verifies that $\text{Base64URL}(\text{SHA256}(V)) == C$.

Even if an attacker intercepts the authorization code in transit, they cannot exchange it for access tokens because they lack the raw code_verifier $V$.

3. Anatomy of a JSON Web Token (JWT)

A JWT consists of three Base64URL-encoded strings separated by dots (Header.Payload.Signature):

// 1. Header (Algorithm & Token Type) { "alg": "RS256", "typ": "JWT", "kid": "key-2026-v1" } // 2. Payload (Registered & Custom Claims) { "iss": "https://auth.mashaewilliamsllc.com", "sub": "user_9842", "aud": "https://api.mashaewilliamsllc.com", "exp": 1771800000, "nbf": 1771796400, "roles": ["admin", "engineer"] } // 3. Signature (Cryptographic Verification) RSASHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), privateKey)

4. Cryptographic Signing: HS256 vs RS256

Signature Type Cryptographic Model Security & Architectural Assessment
HS256 (HMAC SHA-256) Symmetric Shared Secret Both Auth Server and API Gateway must share the exact same secret key. If an API service is compromised, attackers can forge valid JWT tokens.
RS256 / ES256 (RSA / ECDSA) Asymmetric Public/Private Keypair Industry Gold Standard: Auth Server signs with a Private Key; external API services verify signature using the public JWKS (JSON Web Key Set) endpoint. Microservices cannot forge tokens.

5. Refresh Token Rotation & Theft Detection

Because access tokens are short-lived (e.g. 15-minute expiration) to minimize exposure windows, applications use Refresh Tokens to request new access tokens silently.

To prevent stolen refresh tokens from providing infinite access, implement Refresh Token Rotation:

  • Every time a Refresh Token $RT_1$ is exchanged, the auth server invalidates $RT_1$ and returns a brand-new $RT_2$.
  • Automatic Theft Detection: If an attacker uses an already-invalidated $RT_1$, the server detects token reuse, immediately revokes the entire token family, and forces all active user sessions to re-authenticate.

6. Production JavaScript PKCE Code Example

Below is a native browser JavaScript module generating PKCE challenges using Web Cryptography API:

async function generatePKCE() { // 1. Generate Random Code Verifier const array = new Uint8Array(32); window.crypto.getRandomValues(array); const codeVerifier = base64UrlEncode(array); // 2. Hash Code Verifier with SHA-256 const encoder = new TextEncoder(); const data = encoder.encode(codeVerifier); const digest = await window.crypto.subtle.digest('SHA-256', data); const codeChallenge = base64UrlEncode(new Uint8Array(digest)); return { codeVerifier, codeChallenge }; } function base64UrlEncode(buffer) { let str = ""; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { str += String.fromCharCode(bytes[i]); } return btoa(str) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); }

7. Summary Security Rules

  • Never Store JWTs in LocalStorage: LocalStorage is vulnerable to XSS attacks. Store Refresh Tokens in SameSite=Strict; HttpOnly; Secure cookies.
  • Always Validate Claims: Verify exp (expiration), iss (issuer), and aud (audience) claims on every API request.
  • Disable alg: "none" Support: Ensure your JWT verification library explicitly rejects unsigned tokens.