this repo has no description
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}