this repo has no description
at main 88 lines 2.2 kB view raw
1import type { Context, Next } from "hono"; 2import { getCookie, setCookie } from "hono/cookie"; 3 4const CSRF_COOKIE_NAME = "csrf_token"; 5const CSRF_HEADER_NAME = "x-csrf-token"; 6const CSRF_FORM_FIELD = "_csrf"; 7 8/** 9 * Generate a cryptographically secure random token 10 */ 11function generateToken(): string { 12 const buffer = new Uint8Array(32); 13 crypto.getRandomValues(buffer); 14 return Array.from(buffer, (b) => b.toString(16).padStart(2, "0")).join(""); 15} 16 17/** 18 * Get or create a CSRF token for the current session 19 */ 20export function getCSRFToken(c: Context): string { 21 let token = getCookie(c, CSRF_COOKIE_NAME); 22 23 if (!token) { 24 token = generateToken(); 25 setCookie(c, CSRF_COOKIE_NAME, token, { 26 httpOnly: true, 27 secure: process.env.PUBLIC_URL?.startsWith("https") || false, 28 sameSite: "Strict", 29 path: "/", 30 maxAge: 60 * 60 * 24, // 24 hours 31 }); 32 } 33 34 return token; 35} 36 37/** 38 * Middleware to validate CSRF token on POST/PUT/DELETE requests 39 */ 40export async function csrfProtection(c: Context, next: Next) { 41 const method = c.req.method.toUpperCase(); 42 43 // Only check CSRF for state-changing methods 44 if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) { 45 const cookieToken = getCookie(c, CSRF_COOKIE_NAME); 46 47 if (!cookieToken) { 48 return c.text("CSRF token missing", 403); 49 } 50 51 // Check header first (for AJAX requests) 52 let requestToken = c.req.header(CSRF_HEADER_NAME); 53 54 // Fall back to form field 55 if (!requestToken) { 56 const contentType = c.req.header("content-type") || ""; 57 if ( 58 contentType.includes("application/x-www-form-urlencoded") || 59 contentType.includes("multipart/form-data") 60 ) { 61 try { 62 const body = await c.req.parseBody(); 63 requestToken = body[CSRF_FORM_FIELD] as string; 64 } catch { 65 // Body might have already been parsed 66 } 67 } 68 } 69 70 if (!requestToken || requestToken !== cookieToken) { 71 return c.text("CSRF token invalid", 403); 72 } 73 } 74 75 await next(); 76} 77 78import { raw } from "hono/html"; 79 80/** 81 * HTML helper to generate a hidden CSRF input field 82 * Returns a raw HTML string that won't be escaped by Hono's html template 83 */ 84export function csrfField(token: string) { 85 return raw( 86 `<input type="hidden" name="${CSRF_FORM_FIELD}" value="${token}" />`, 87 ); 88}