95 lines
2.1 KiB
TypeScript
95 lines
2.1 KiB
TypeScript
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<string> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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<void> {
|
|
const cookieStore = await cookies();
|
|
|
|
try {
|
|
cookieStore.delete(CSRF_TOKEN_COOKIE_NAME);
|
|
} catch {
|
|
// Handle server component context
|
|
}
|
|
}
|