Master OAuth 2.0 and OIDC for secure authentication. Learn authorization code flow, PKCE, refresh tokens, JWT validation, and secure implementation patterns.
OAuth 2.0 and OpenID Connect
OAuth 2.0 Authorization Code Flow with PKCE
import crypto from 'crypto'
// Step 1: Generate PKCE code verifier and challenge
function generatePKCE() {
const verifier = crypto.randomBytes(32).toString('base64url')
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
return { verifier, challenge }
}
// Step 2: Build authorization URL
function buildAuthURL(config, codeChallenge, state) {
const params = new URLSearchParams({
response_type: 'code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
scope: 'openid profile email',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
})
return `${config.authorizationEndpoint}?${params}`
}
// Step 3: Exchange code for tokens
async function exchangeCode(code, codeVerifier, config) {
const resp = await fetch(config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.clientId,
redirect_uri: config.redirectUri,
code,
code_verifier: codeVerifier,
}),
})
return resp.json()
}
JWT Validation
import jwt from 'jsonwebtoken'
import jwksClient from 'jwks-rsa'
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json',
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 600000, // 10 minutes
})
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err)
callback(null, key.getPublicKey())
})
}
function validateToken(token, options = {}) {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
audience: process.env.CLIENT_ID,
...options,
}, (err, decoded) => {
if (err) reject(err)
else resolve(decoded)
})
})
}
Refresh Token Rotation
class TokenManager {
constructor(config) {
this.config = config
this.tokens = null
}
async getAccessToken() {
if (!this.tokens) throw new Error('Not authenticated')
if (this.isExpired()) {
await this.refresh()
}
return this.tokens.access_token
}
isExpired() {
return Date.now() >= this.tokens.expires_at - 60000 // 1 min buffer
}
async refresh() {
const resp = await fetch(this.config.tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
client_id: this.config.clientId,
refresh_token: this.tokens.refresh_token,
}),
})
if (!resp.ok) {
this.tokens = null // Force re-login
throw new Error('Token refresh failed')
}
const data = await resp.json()
this.tokens = {
...data,
expires_at: Date.now() + data.expires_in * 1000,
}
}
}
Express.js OAuth Middleware
import express from 'express'
import session from 'express-session'
import { Issuer, generators } from 'openid-client'
const app = express()
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }))
let client
async function setupOIDC() {
const issuer = await Issuer.discover('https://accounts.google.com')
client = new issuer.Client({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uris: ['http://localhost:3000/callback'],
response_types: ['code'],
})
}
app.get('/login', (req, res) => {
const state = generators.state()
const nonce = generators.nonce()
const codeVerifier = generators.codeVerifier()
const codeChallenge = generators.codeChallenge(codeVerifier)
req.session.auth = { state, nonce, codeVerifier }
const authUrl = client.authorizationUrl({
scope: 'openid email profile',
state, nonce, code_challenge: codeChallenge,
code_challenge_method: 'S256',
})
res.redirect(authUrl)
})
app.get('/callback', async (req, res) => {
const params = client.callbackParams(req)
const { state, nonce, codeVerifier } = req.session.auth
const tokenSet = await client.callback(
'http://localhost:3000/callback',
params, { state, nonce, code_verifier: codeVerifier }
)
const userinfo = await client.userinfo(tokenSet.access_token)
req.session.user = userinfo
res.redirect('/dashboard')
})
// Protected route middleware
function requireAuth(req, res, next) {
if (!req.session.user) return res.redirect('/login')
next()
}
app.get('/dashboard', requireAuth, (req, res) => {
res.json({ user: req.session.user })
})
Security Best Practices
| Concern |
Mitigation |
| CSRF |
State parameter + SameSite cookies |
| Token theft |
Short-lived access tokens (15 min) |
| Refresh token reuse |
Token rotation + detect reuse |
| Redirect URI |
Exact match validation |
| State validation |
Cryptographic random state |