A minimal web editor for managing standard.site records in your atproto PDS
at main 2.4 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 (contentType.includes('application/x-www-form-urlencoded') || 58 contentType.includes('multipart/form-data')) { 59 try { 60 const body = await c.req.parseBody(); 61 requestToken = body[CSRF_FORM_FIELD] as string; 62 } catch { 63 // Body might have already been parsed 64 } 65 } 66 } 67 68 if (!requestToken || requestToken !== cookieToken) { 69 return c.text('CSRF token invalid', 403); 70 } 71 } 72 73 await next(); 74} 75 76import { raw } from 'hono/html'; 77 78/** 79 * HTML helper to generate a hidden CSRF input field 80 * Returns a raw HTML string that won't be escaped by Hono's html template 81 */ 82export function csrfField(token: string) { 83 return raw(`<input type="hidden" name="${CSRF_FORM_FIELD}" value="${token}" />`); 84}