OAuth 2.0 and JWT are used in virtually every modern web application — and misimplemented in most of them. Storing tokens in localStorage, using symmetric secrets in distributed systems, skipping algorithm verification, and never rotating refresh tokens are all real vulnerabilities in production systems. This guide explains how authentication actually works and how to get it right. This guide focuses on OAuth 2.0 JWT, with the production trade-offs senior engineers need to make sound decisions.
⚡ TL;DR: OAuth 2.0 is an authorization framework. JWT is a token format. Use Authorization Code + PKCE flow for all clients. Store tokens in httpOnly cookies (not localStorage). Verify JWT algorithm explicitly. Use short-lived access tokens (15min) with refresh token rotation.
Authorization Code + PKCE flow — OAuth 2.0 JWT
// PKCE (Proof Key for Code Exchange) — prevents auth code interception
// Required for: SPAs, mobile apps, any public client
// Step 1: Generate code verifier + challenge
const crypto = require('crypto');
const codeVerifier = crypto.randomBytes(32).toString('base64url');
const codeChallenge = crypto.createHash('sha256')
.update(codeVerifier).digest('base64url');
// Step 2: Redirect user to auth server
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', randomState); // CSRF protection
// Step 3: Exchange code for tokens (on callback)
const tokens = await fetch('https://auth.example.com/token', {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code',
code: req.query.code,
code_verifier: codeVerifier, // Proves we initiated the flow
client_id: CLIENT_ID,
redirect_uri: 'https://myapp.com/callback'
})
}).then(r => r.json());JWT structure and verification
const jwt = require('jsonwebtoken');
// JWT = header.payload.signature (base64url encoded)
// Header: { alg: 'HS256', typ: 'JWT' }
// Payload: { sub: 'user123', iat: 1712534400, exp: 1712535300, role: 'admin' }
// Signature: HMACSHA256(base64(header) + '.' + base64(payload), secret)
// NEVER DO THIS:
const decoded = jwt.decode(token); // No verification! Attacker can forge!
// CORRECT: always verify
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // ALWAYS specify! Prevents alg:none attack
issuer: 'https://auth.example.com',
audience: 'https://api.example.com'
});
req.user = payload;
} catch (err) {
if (err.name === 'TokenExpiredError') return res.status(401).json({ code: 'TOKEN_EXPIRED' });
return res.status(401).json({ code: 'INVALID_TOKEN' });
}Token storage — httpOnly cookies not localStorage
// WRONG: localStorage — vulnerable to XSS
localStorage.setItem('access_token', token);
// Any injected script can read this!
fetch('/api', { headers: { Authorization: 'Bearer ' + localStorage.getItem('access_token') } });
// RIGHT: httpOnly cookie — inaccessible to JavaScript
res.cookie('access_token', accessToken, {
httpOnly: true, // Can't be read by JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 15 * 60 * 1000 // 15 minutes
});
// Browser sends cookie automatically on every request
// JavaScript cannot read it — XSS cannot steal it
// Refresh token rotation:
app.post('/auth/refresh', async (req, res) => {
const refreshToken = req.cookies.refresh_token;
const payload = await verifyRefreshToken(refreshToken);
await revokeRefreshToken(refreshToken); // Invalidate old one
const newRefresh = generateRefreshToken(payload.sub);
const newAccess = generateAccessToken(payload.sub);
res.cookie('refresh_token', newRefresh, { httpOnly: true, secure: true });
res.json({ access_token: newAccess });
});- ✅ Authorization Code + PKCE for all clients — never implicit flow
- ✅ httpOnly cookies for token storage — never localStorage
- ✅ Explicit algorithm in jwt.verify() — prevents alg:none attack
- ✅ Short-lived access tokens (15min) + refresh token rotation
- ✅ Verify issuer and audience claims in JWT
- ❌ Never use jwt.decode() without jwt.verify()
- ❌ Never store tokens in localStorage or sessionStorage
OAuth and JWT authentication is a critical part of the API security guide. For AWS-native auth, Cognito implements these flows with managed infrastructure. External reference: OAuth 2.0 specification.
Recommended Reading
→ Designing Data-Intensive Applications — The essential book every senior developer needs.
→ The Pragmatic Programmer — Timeless engineering wisdom for writing better code.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Don’t Miss the Next Cheat Code
Join 1,000+ senior developers getting expert JS, Python, AWS and system design secrets weekly.
Related: TypeScript for JavaScript Developers: The Migration Guide That Actually Works · How HTTPS Works: TLS, Certificates, and Encryption Explained for Developers · JavaScript Prototype Chain: How Inheritance Really Works in JS
Related: Time-Saving One-Liners Every Developer Should Know · API Debugging in 30 Seconds: The Fastest Ways to Fix API Issues
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
