import { createKV } from "./create-kv.js"; import { getCookie } from "hono/cookie"; import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; export const sessionsKV = createKV("sessions"); export const usersKV = createKV("users"); // Cookie configuration constants export const SESSION_COOKIE_NAME = 'sessionId'; export const CSRF_COOKIE_NAME = 'csrf-token'; export const COOKIE_OPTIONS = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'Lax', path: '/', maxAge: 86400 // 24 hours }; // CSRF token generation export function generateCSRFToken() { return randomBytes(32).toString('hex'); } // Session extraction from cookies export async function getSessionFromCookies(c) { const sessionId = getCookie(c, SESSION_COOKIE_NAME); if (!sessionId) return null; const session = await sessionsKV.getItem(sessionId); if (!session) return null; // Check expiration if (new Date(session.expiresAt) < new Date()) { // Clean up expired session await sessionsKV.removeItem(sessionId); return null; } return session; } // CSRF token validation export async function validateCSRFToken(c, sessionId) { const headerToken = c.req.header('X-CSRF-Token'); if (!headerToken) throw new Error("CSRF token missing"); const session = await sessionsKV.getItem(sessionId); if (!session?.csrfToken) throw new Error("Invalid session"); // Use timing-safe comparison to prevent timing attacks const sessionTokenBuffer = Buffer.from(session.csrfToken, 'hex'); const headerTokenBuffer = Buffer.from(headerToken, 'hex'); if (sessionTokenBuffer.length !== headerTokenBuffer.length || !timingSafeEqual(sessionTokenBuffer, headerTokenBuffer)) { throw new Error("CSRF token mismatch"); } } // Session rotation helper export async function rotateSession(oldSessionId, userId) { // Delete old session await sessionsKV.removeItem(oldSessionId); // Create new session with CSRF token const newSessionId = randomUUID(); const csrfToken = generateCSRFToken(); const now = new Date(); const expiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1000); // 24 hours const newSession = { id: newSessionId, userId, expiresAt: expiresAt.toISOString(), createdAt: now.toISOString(), csrfToken }; await sessionsKV.setItem(newSessionId, newSession); return newSession; } // Updated assertOwner function with CSRF validation export async function assertOwner(c) { const session = await getSessionFromCookies(c); if (!session) throw new Error("Invalid session"); const user = await usersKV.getItem(session.userId); if (!user || user.role !== "owner") throw new Error("Forbidden"); // Validate CSRF token for non-GET requests const method = c.req.method; if (method !== 'GET' && method !== 'HEAD') { await validateCSRFToken(c, session.id); } }