- Comply with OAuth 2.1 by deprecating the Implicit Grant in favor of Authorization Code with PKCE.
- Never store access or refresh tokens in localStorage where XSS exploits can exfiltrate them.
- Implement Refresh Token Rotation with automatic family revocation to mitigate token replay risks.
- Deploy the Backend-for-Frontend (BFF) architecture to isolate tokens from the browser environment.
1. Why the Implicit Grant Was Deprecated
In early OAuth 2.0 specifications, Single Page Applications relied on the Implicit Grant flow, which returned access tokens directly in the URL hash fragment (#access_token=...). This exposed raw JWT credentials in browser history, server access logs, and referrer headers.
OAuth 2.1 officially deprecates the Implicit Grant flow entirely. All public clients—including SPAs and mobile apps—must implement the Authorization Code Flow augmented with Proof Key for Code Exchange (PKCE).
2. The Cryptographic Mechanics of PKCE
Public clients cannot safely store a static client_secret without risk of reverse engineering. PKCE dynamically generates a cryptographic one-time secret for every single authentication handshake.
The client creates a cryptographically random code_verifier (43-128 characters) and computes its SHA-256 hash, known as the code_challenge. The authorization server retains this challenge during login and validates the raw verifier upon code exchange.
// Generate cryptographic random verifier and SHA-256 challenge
export async function generatePKCECodes() {
const array = new Uint8Array(64);
window.crypto.getRandomValues(array);
const codeVerifier = Array.from(array, dec => dec.toString(16).padStart(2, '0')).join('');
// Compute SHA-256 hash buffer
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
// Base64URL encode the challenge
const hashArray = Array.from(new Uint8Array(hashBuffer));
const base64 = btoa(String.fromCharCode(...hashArray));
const codeChallenge = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return { codeVerifier, codeChallenge };
}3. Token Storage: LocalStorage vs. HttpOnly Secure Cookies
Storing JWT access tokens in localStorage or sessionStorage leaves them vulnerable: any third-party script injected via a cross-site scripting (XSS) vulnerability or malicious npm dependency can read window.localStorage and exfiltrate credentials.
Store tokens in HttpOnly, Secure, SameSite=Strict cookies. These cookies are inaccessible to browser JavaScript, rendering token exfiltration via client-side scripts physically impossible.
4. Refresh Token Rotation and Replay Detection
Because access tokens should expire quickly (e.g., 10 to 15 minutes), refresh tokens extend user sessions. Under Refresh Token Rotation, the authorization server invalidates the old refresh token and issues a new pair on every single refresh.
If an invalidated refresh token is ever presented again, the auth server identifies a token theft replay attack and immediately revokes all sessions associated with that user family.
5. The Backend-for-Frontend (BFF) Security Paradigm
For maximum enterprise security, deploy the Backend-for-Frontend (BFF) proxy pattern. The SPA communicates only with a lightweight backend proxy (e.g., Next.js API route or Node gateway) using session cookies.
The BFF proxy securely holds client secrets, handles PKCE validation, and injects bearer tokens into upstream microservice calls, keeping raw cryptographic tokens completely isolated from browser contexts.
6. Frequently Asked Questions
Technical FAQ
Q:Does SameSite=Strict cookies prevent Cross-Site Request Forgery (CSRF)?
Yes. SameSite=Strict ensures cookies are only included in requests originating from the exact same registered site, neutralizing CSRF attacks from external links.
Q:What is the recommended lifespan for enterprise access tokens?
Between 5 and 15 minutes. Short lifespans limit the window of opportunity if a token is intercepted in transit.
Muhammad Umer Farooq
Co-Founder & Chief Executive Officer
Co-Founder & CEO of Khizex. Oversees company strategy, enterprise system architecture, distributed backends, and full-stack product engineering.