OAuth 2.0 and JWT Explained: How Authentication Actually Works in Production

OAuth 2.0 and JWT Explained: How Authentication Actually Works in Production

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.

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

// 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.

✓ No spam✓ Unsubscribe anytime

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply