Initial commit
This commit is contained in:
104
server/utils/auth.ts
Normal file
104
server/utils/auth.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user