import { cookies } from 'next/headers'; import { randomBytes } from 'crypto'; const CSRF_TOKEN_COOKIE_NAME = 'csrf_token'; const CSRF_TOKEN_LENGTH = 32; /** * Generate a cryptographically secure CSRF token */ export function generateCsrfToken(): string { return randomBytes(CSRF_TOKEN_LENGTH).toString('base64url'); } /** * Get the current CSRF token from cookies or generate a new one */ export async function getCsrfToken(): Promise { const cookieStore = await cookies(); const existingToken = cookieStore.get(CSRF_TOKEN_COOKIE_NAME); if (existingToken?.value) { return existingToken.value; } const newToken = generateCsrfToken(); await setCsrfToken(newToken); return newToken; } /** * Set the CSRF token in an httpOnly cookie */ export async function setCsrfToken(token: string): Promise { const cookieStore = await cookies(); try { cookieStore.set(CSRF_TOKEN_COOKIE_NAME, token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 60 * 60 * 24, // 24 hours path: '/', }); } catch { // Handle server component context where cookies can't be set } } /** * Validate a CSRF token against the stored token in cookies */ export async function validateCsrfToken(token: string): Promise { const cookieStore = await cookies(); const storedToken = cookieStore.get(CSRF_TOKEN_COOKIE_NAME); if (!storedToken?.value || !token) { return false; } // Constant-time comparison to prevent timing attacks return timingSafeEqual( Buffer.from(storedToken.value), Buffer.from(token) ); } /** * Timing-safe string comparison to prevent timing attacks */ function timingSafeEqual(a: Buffer, b: Buffer): boolean { if (a.length !== b.length) { return false; } let result = 0; for (let i = 0; i < a.length; i++) { result |= a[i] ^ b[i]; } return result === 0; } /** * Delete the CSRF token cookie */ export async function deleteCsrfToken(): Promise { const cookieStore = await cookies(); try { cookieStore.delete(CSRF_TOKEN_COOKIE_NAME); } catch { // Handle server component context } }