OAuth2 & Authentication Patterns
Overview
Production patterns for implementing authentication using OAuth 2.0, OpenID Connect, JWT, and session-based auth. Covers social login integration, token management, and security best practices.
When to Use
- Implementing "Sign in with Google/GitHub/Apple"
- Building custom auth with JWT + refresh tokens
- Session-based authentication for server-rendered apps
- Multi-tenant applications with SSO
Authorization Code Flow with PKCE
// 1. Generate PKCE challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url')
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
return { verifier, challenge }
}
// 2. Redirect to provider
app.get('/auth/login', (req, res) => {
const { verifier, challenge } = generatePKCE()
req.session.codeVerifier = verifier
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: crypto.randomBytes(16).toString('hex'),
code_challenge: challenge,
code_challenge_method: 'S256',
})
res.redirect(`https://provider.com/authorize?${params}`)
})
// 3. Handle callback
app.get('/auth/callback', async (req, res) => {
const { code, state } = req.query
// Exchange code for tokens
const tokenResponse = await fetch('https://provider.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code as string,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code_verifier: req.session.codeVerifier,
}),
})
const { access_token, id_token, refresh_token } = await tokenResponse.json()
// Verify id_token, create session, store refresh_token securely
})
JWT Pattern
import jwt from 'jsonwebtoken'
// Create tokens
function createTokens(userId: string) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
)
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
)
return { accessToken, refreshToken }
}
// Verify middleware
function authMiddleware(req, res, next) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) return res.status(401).json({ error: 'No token' })
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!)
req.user = payload
next()
} catch {
return res.status(401).json({ error: 'Invalid token' })
}
}
// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body
try {
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!)
// Check if refresh token is revoked (stored in DB or Redis)
const isRevoked = await checkRevoked(refreshToken)
if (isRevoked) return res.status(401).json({ error: 'Token revoked' })
const tokens = createTokens(payload.sub)
// Rotate: revoke old refresh token
await revokeToken(refreshToken)
res.json(tokens)
} catch {
return res.status(401).json({ error: 'Invalid refresh token' })
}
})
Guidelines
- Always use PKCE for authorization code flow (even for confidential clients)
- Short-lived access tokens (15 min), long-lived refresh tokens (7 days)
- Rotate refresh tokens — revoke the old one when issuing a new one
- Store refresh tokens server-side (database or Redis), never in localStorage
- Use
httpOnly, secure, sameSite cookies for web session tokens
- Verify
id_token signature before trusting claims
Anti-Patterns
- ❌ Storing JWTs in localStorage (XSS vulnerable)
- ❌ Long-lived access tokens (use refresh tokens instead)
- ❌ Not rotating refresh tokens (allows stolen tokens to persist)
- ❌ Skipping PKCE ("my app is server-side so I don't need it")
- ❌ Putting sensitive data in JWT payload (it's base64, not encrypted)
1---2name: oauth2-auth-patterns3description: OAuth 2.0 and OpenID Connect authentication patterns. Covers authorization code flow, PKCE, JWT, session management, and provider integration.4---56# OAuth2 & Authentication Patterns78## Overview910Production patterns for implementing authentication using OAuth 2.0, OpenID Connect, JWT, and session-based auth. Covers social login integration, token management, and security best practices.1112## When to Use1314- Implementing "Sign in with Google/GitHub/Apple"15- Building custom auth with JWT + refresh tokens16- Session-based authentication for server-rendered apps17- Multi-tenant applications with SSO1819## Authorization Code Flow with PKCE2021```typescript22// 1. Generate PKCE challenge23function generatePKCE() {24 const verifier = crypto.randomBytes(32).toString('base64url')25 const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')26 return { verifier, challenge }27}2829// 2. Redirect to provider30app.get('/auth/login', (req, res) => {31 const { verifier, challenge } = generatePKCE()32 req.session.codeVerifier = verifier3334 const params = new URLSearchParams({35 response_type: 'code',36 client_id: CLIENT_ID,37 redirect_uri: REDIRECT_URI,38 scope: 'openid profile email',39 state: crypto.randomBytes(16).toString('hex'),40 code_challenge: challenge,41 code_challenge_method: 'S256',42 })4344 res.redirect(`https://provider.com/authorize?${params}`)45})4647// 3. Handle callback48app.get('/auth/callback', async (req, res) => {49 const { code, state } = req.query5051 // Exchange code for tokens52 const tokenResponse = await fetch('https://provider.com/token', {53 method: 'POST',54 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },55 body: new URLSearchParams({56 grant_type: 'authorization_code',57 code: code as string,58 redirect_uri: REDIRECT_URI,59 client_id: CLIENT_ID,60 client_secret: CLIENT_SECRET,61 code_verifier: req.session.codeVerifier,62 }),63 })6465 const { access_token, id_token, refresh_token } = await tokenResponse.json()66 // Verify id_token, create session, store refresh_token securely67})68```6970## JWT Pattern7172```typescript73import jwt from 'jsonwebtoken'7475// Create tokens76function createTokens(userId: string) {77 const accessToken = jwt.sign(78 { sub: userId, type: 'access' },79 process.env.JWT_SECRET!,80 { expiresIn: '15m' }81 )82 83 const refreshToken = jwt.sign(84 { sub: userId, type: 'refresh' },85 process.env.JWT_REFRESH_SECRET!,86 { expiresIn: '7d' }87 )8889 return { accessToken, refreshToken }90}9192// Verify middleware93function authMiddleware(req, res, next) {94 const token = req.headers.authorization?.replace('Bearer ', '')95 if (!token) return res.status(401).json({ error: 'No token' })9697 try {98 const payload = jwt.verify(token, process.env.JWT_SECRET!)99 req.user = payload100 next()101 } catch {102 return res.status(401).json({ error: 'Invalid token' })103 }104}105106// Refresh endpoint107app.post('/auth/refresh', async (req, res) => {108 const { refreshToken } = req.body109 try {110 const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!)111 // Check if refresh token is revoked (stored in DB or Redis)112 const isRevoked = await checkRevoked(refreshToken)113 if (isRevoked) return res.status(401).json({ error: 'Token revoked' })114115 const tokens = createTokens(payload.sub)116 // Rotate: revoke old refresh token117 await revokeToken(refreshToken)118 res.json(tokens)119 } catch {120 return res.status(401).json({ error: 'Invalid refresh token' })121 }122})123```124125## Guidelines1261271. **Always use PKCE** for authorization code flow (even for confidential clients)1282. **Short-lived access tokens** (15 min), long-lived refresh tokens (7 days)1293. **Rotate refresh tokens** — revoke the old one when issuing a new one1304. **Store refresh tokens server-side** (database or Redis), never in localStorage1315. **Use `httpOnly`, `secure`, `sameSite` cookies** for web session tokens1326. **Verify `id_token` signature** before trusting claims133134## Anti-Patterns135136- ❌ Storing JWTs in localStorage (XSS vulnerable)137- ❌ Long-lived access tokens (use refresh tokens instead)138- ❌ Not rotating refresh tokens (allows stolen tokens to persist)139- ❌ Skipping PKCE ("my app is server-side so I don't need it")140- ❌ Putting sensitive data in JWT payload (it's base64, not encrypted)