// server/utils/pkce.ts /** * PKCE (Proof Key for Code Exchange) utilities for OAuth2 security. * * PKCE prevents authorization code interception attacks by requiring * the client to prove possession of the original code verifier. * * Flow: * 1. Generate random code_verifier (43-128 chars) * 2. Hash verifier with SHA-256 → code_challenge * 3. Send challenge to authorization server * 4. Server returns authorization code * 5. Exchange code + verifier for tokens * 6. Server validates: SHA256(verifier) === stored_challenge */ /** * Generate a random code verifier (43-128 URL-safe characters) * * @param length - Length of verifier (default: 64) * @returns Base64URL-encoded random string */ export function generateCodeVerifier(length: number = 64): string { // Generate random bytes const randomBytes = crypto.getRandomValues(new Uint8Array(length)) // Convert to base64url (URL-safe base64 without padding) return base64UrlEncode(randomBytes) } /** * Generate SHA-256 hash of code verifier → code challenge * * @param verifier - The code verifier * @returns Base64URL-encoded SHA-256 hash */ export async function generateCodeChallenge(verifier: string): Promise { // Convert verifier string to Uint8Array const encoder = new TextEncoder() const data = encoder.encode(verifier) // Hash with SHA-256 const hashBuffer = await crypto.subtle.digest('SHA-256', data) // Convert to base64url return base64UrlEncode(new Uint8Array(hashBuffer)) } /** * Generate PKCE verifier + challenge pair * * @returns Object with verifier and challenge */ export async function generatePKCE(): Promise<{ verifier: string challenge: string }> { const verifier = generateCodeVerifier() const challenge = await generateCodeChallenge(verifier) return { verifier, challenge } } /** * Convert Uint8Array to Base64URL string * * Base64URL is URL-safe variant of Base64: * - Replace '+' with '-' * - Replace '/' with '_' * - Remove padding '=' */ function base64UrlEncode(buffer: Uint8Array): string { // Convert to base64 const base64 = btoa(String.fromCharCode(...buffer)) // Convert to base64url return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') } /** * Generate random state parameter for CSRF protection * * @param length - Length of state string (default: 32) * @returns Random URL-safe string */ export function generateState(length: number = 32): string { const randomBytes = crypto.getRandomValues(new Uint8Array(length)) return base64UrlEncode(randomBytes) }