To secure OAuth integrations, you need to: (1) always implement PKCE flow even for server-side applications, (2) validate state parameters to prevent CSRF attacks, (3) exchange authorization codes server-side only where client secrets are secure, (4) verify ID tokens using the provider's JWKS before trusting claims, and (5) explicitly configure redirect URIs in your OAuth provider. This blueprint covers secure OAuth patterns for any provider integration.
TL;DR
Always use PKCE (even for server-side apps), validate state parameters to prevent CSRF, exchange codes server-side only, and verify tokens before trusting their contents. Use established libraries instead of implementing OAuth from scratch.
PKCE Flow Implementation OAuth 2.0
import crypto from 'crypto'
function generateCodeVerifier(): string {
return crypto.randomBytes(32).toString('base64url')
}
function generateCodeChallenge(verifier: string): string {
return crypto.createHash('sha256').update(verifier).digest('base64url')
}
export async function initiateOAuth(provider: 'github' | 'google') {
const state = crypto.randomBytes(16).toString('hex')
const codeVerifier = generateCodeVerifier()
const codeChallenge = generateCodeChallenge(codeVerifier)
// Store in session/cookie (encrypted)
await storeOAuthState({ state, codeVerifier, provider })
const params = new URLSearchParams({
client_id: process.env[`${provider.toUpperCase()}_CLIENT_ID`]!,
redirect_uri: `${process.env.BASE_URL}/api/auth/callback/${provider}`,
scope: 'openid profile email',
response_type: 'code',
state,
code_challenge: codeChallenge,
code_challenge_method: 'S256',
})
return `https://provider.example.com/authorize?${params}`
}
Callback Handler
export async function GET(
req: Request,
{ params }: { params: { provider: string } }
) {
const url = new URL(req.url)
const code = url.searchParams.get('code')
const state = url.searchParams.get('state')
const error = url.searchParams.get('error')
if (error) {
return Response.redirect('/login?error=oauth_failed')
}
// Retrieve stored state
const storedState = await getOAuthState()
// CRITICAL: Validate state to prevent CSRF
if (!storedState || storedState.state !== state) {
return Response.redirect('/login?error=invalid_state')
}
// Exchange code for tokens (server-side only!)
const tokenResponse = await fetch('https://provider.example.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code!,
redirect_uri: `${process.env.BASE_URL}/api/auth/callback/${params.provider}`,
client_id: process.env.CLIENT_ID!,
client_secret: process.env.CLIENT_SECRET!,
code_verifier: storedState.codeVerifier, // PKCE
}),
})
const tokens = await tokenResponse.json()
// Verify ID token before trusting it
const user = await verifyIdToken(tokens.id_token)
// Create session
await createSession(user)
return Response.redirect('/dashboard')
}
Token Verification OAuth 2.0
import * as jose from 'jose'
export async function verifyIdToken(idToken: string) {
// Fetch provider's public keys
const JWKS = jose.createRemoteJWKSet(
new URL('https://provider.example.com/.well-known/jwks.json')
)
try {
const { payload } = await jose.jwtVerify(idToken, JWKS, {
issuer: 'https://provider.example.com',
audience: process.env.CLIENT_ID!,
})
// Verify required claims
if (!payload.sub || !payload.email) {
throw new Error('Missing required claims')
}
return {
id: payload.sub,
email: payload.email as string,
name: payload.name as string,
}
} catch (error) {
throw new Error('Invalid ID token')
}
}
Never exchange codes client-side. The authorization code must be exchanged for tokens on your server, where the client secret is secure. PKCE adds protection but doesn't replace server-side exchange.
Security Checklist
Pre-Launch Checklist
PKCE implemented (code_challenge)
State parameter validated
Code exchange server-side only
ID tokens verified before use
Redirect URIs explicitly configured
Client secret stored securely
Related Integration Stacks
Auth0 Managed OAuth Clerk OAuth Integration NextAuth Self-Hosted OAuth