import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; import type { H3Event } from "h3"; import { createError, deleteCookie, getCookie, getRequestHeader, setCookie, } from "h3"; const COOKIE = "quickdrop_auth"; const DAY = 86400000; function config() { const runtime = useRuntimeConfig(); const configuredTtl = Number( process.env.AUTH_TOKEN_TTL_DAYS || runtime.authTokenTtlDays || 7, ); const ttlDays = Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 7; return { passcode: String(process.env.AUTH_PASSCODE || runtime.authPasscode || ""), secret: String(process.env.AUTH_SECRET || runtime.authSecret || ""), ttl: ttlDays * DAY, }; } function mac(value: string, secret: string) { return createHmac("sha256", secret).update(value).digest("base64url"); } export function authConfigured() { const c = config(); return Boolean(c.passcode && c.secret); } export function issueSession(event: H3Event) { const c = config(); if (!c.secret) throw new Error("Authentication is not configured."); const payload = `${Date.now() + c.ttl}.${randomBytes(24).toString("base64url")}`; setCookie(event, COOKIE, `${payload}.${mac(payload, c.secret)}`, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: Math.floor(c.ttl / 1000), }); } export function clearAuthCookie(event: H3Event) { deleteCookie(event, COOKIE, { path: "/" }); } export function isAuthenticated(event: H3Event): boolean { const c = config(); const token = getCookie(event, COOKIE); if (!token || !c.secret) return false; const split = token.lastIndexOf("."); if (split < 1) return false; const payload = token.slice(0, split); const supplied = Buffer.from(token.slice(split + 1)); const expected = Buffer.from(mac(payload, c.secret)); if ( supplied.length !== expected.length || !timingSafeEqual(supplied, expected) ) return false; const expiry = Number(payload.slice(0, payload.indexOf("."))); return Number.isSafeInteger(expiry) && expiry > Date.now(); } export function requireAuth(event: H3Event) { if (!isAuthenticated(event)) throw createError({ statusCode: 401, statusMessage: "Authentication required.", }); requireSameOrigin(event); } export function verifyPasscode(value: unknown): boolean { const c = config(); if (!c.passcode || typeof value !== "string") return false; const supplied = Buffer.from(value); const expected = Buffer.from(c.passcode); return ( supplied.length === expected.length && timingSafeEqual(supplied, expected) ); } export function requireSameOrigin(event: H3Event) { const origin = getRequestHeader(event, "origin"); const site = getRequestHeader(event, "sec-fetch-site"); if ( site && ["cross-site", "same-site"].includes(site) && site === "cross-site" ) throw createError({ statusCode: 403, statusMessage: "Cross-site state change rejected.", }); if (origin) { const host = getRequestHeader(event, "host"); const proto = getRequestHeader(event, "x-forwarded-proto") || "http"; try { if (new URL(origin).host !== host || !["http", "https"].includes(proto)) throw new Error(); } catch { throw createError({ statusCode: 403, statusMessage: "Origin rejected." }); } } } export { COOKIE };