40464-vm/backend/src/security/cookieAuth.js
2026-07-22 15:25:42 +00:00

79 lines
2.0 KiB
JavaScript

const SESSION_COOKIE = "coaching_session";
function parseCookieHeader(header) {
const cookies = {};
for (const part of String(header || "").split(";")) {
const separator = part.indexOf("=");
if (separator === -1) continue;
const name = part.slice(0, separator).trim();
const value = part.slice(separator + 1).trim();
if (name) cookies[name] = decodeURIComponent(value);
}
return cookies;
}
function sessionTokenFromRequest(req) {
return parseCookieHeader(req.headers?.cookie)[SESSION_COOKIE] || null;
}
function cookieOptions() {
return {
httpOnly: true,
secure: process.env.NODE_ENV === "production" || process.env.NODE_ENV === "dev_stage",
sameSite: "lax",
path: "/",
};
}
function setSessionCookie(req, res, token) {
res.cookie(SESSION_COOKIE, token, { ...cookieOptions(), maxAge: 30 * 60 * 1000 });
}
function clearSessionCookie(req, res) {
res.clearCookie(SESSION_COOKIE, cookieOptions());
}
function requestOrigin(req) {
const origin = req.get("origin");
if (origin) return origin;
const referer = req.get("referer");
return referer ? new URL(referer).origin : null;
}
function expectedOrigin(req) {
const protocol = req.get("x-forwarded-proto") || req.protocol;
const host = req.get("x-forwarded-host") || req.get("host");
return `${protocol}://${host}`;
}
function protectCookieMutations(req, res, next) {
if (["GET", "HEAD", "OPTIONS"].includes(req.method)) {
next();
return;
}
if (req.get("authorization") || !sessionTokenFromRequest(req)) {
next();
return;
}
let origin;
try {
origin = requestOrigin(req);
} catch {
res.status(403).send({ error: "csrf_origin_invalid" });
return;
}
if (!origin || origin !== expectedOrigin(req)) {
res.status(403).send({ error: "csrf_origin_invalid" });
return;
}
next();
}
module.exports = { SESSION_COOKIE, parseCookieHeader, sessionTokenFromRequest, setSessionCookie, clearSessionCookie, protectCookieMutations };