@csrf-armor/core
Version:
Framework-agnostic CSRF protection core functionality
127 lines (125 loc) • 4.19 kB
JavaScript
import { OriginMismatchError } from "./errors.mjs";
import { parseSignedToken, timingSafeEqual, verifySignedToken } from "./crypto.mjs";
import { SAFE_METHODS } from "./constants.mjs";
//#region src/validation.ts
function getHeaders(request) {
if (request.headers instanceof Map) return request.headers;
return new Map(Object.entries(request.headers));
}
function getCookies(request) {
if (request.cookies instanceof Map) return request.cookies;
return new Map(Object.entries(request.cookies));
}
async function validateSignedToken(request, config, getTokenFromRequest) {
try {
const token = await getTokenFromRequest(request, config);
if (!token) return {
isValid: false,
reason: "No CSRF token provided"
};
await parseSignedToken(token, config.secret);
return { isValid: true };
} catch (error) {
if (error instanceof Error) return {
isValid: false,
reason: error.message
};
return {
isValid: false,
reason: "Unknown error"
};
}
}
function validateOrigin(request, config) {
const headers = getHeaders(request);
const origin = headers.get("origin");
const referer = headers.get("referer");
if (!origin && !referer && !SAFE_METHODS.includes(request.method)) return {
isValid: false,
reason: "Missing origin and referer headers"
};
const requestOrigin = origin ?? (referer ? new URL(referer).origin : null);
if (!requestOrigin) return {
isValid: false,
reason: "No origin or referer header"
};
if (config.allowedOrigins.includes(requestOrigin)) return { isValid: true };
return {
isValid: false,
reason: new OriginMismatchError(requestOrigin).message
};
}
async function validateDoubleSubmit(request, config, getTokenFromRequest) {
const cookies = getCookies(request);
const cookieName = config.cookie.name;
const cookieToken = cookies.get(cookieName);
const submittedToken = await getTokenFromRequest(request, config);
if (!cookieToken) return {
isValid: false,
reason: "No CSRF cookie found"
};
if (!submittedToken) return {
isValid: false,
reason: "No CSRF token submitted"
};
if (!timingSafeEqual(cookieToken, submittedToken)) return {
isValid: false,
reason: "Token mismatch"
};
return { isValid: true };
}
async function validateSignedDoubleSubmit(request, config, getTokenFromRequest) {
const cookies = getCookies(request);
const cookieName = config.cookie.name;
const unsignedCookieToken = cookies.get(cookieName);
const signedCookieToken = cookies.get(`${cookieName}-server`);
const submittedToken = await getTokenFromRequest(request, config);
if (!unsignedCookieToken || !signedCookieToken) return {
isValid: false,
reason: "Missing CSRF cookies"
};
if (!submittedToken) return {
isValid: false,
reason: "No CSRF token submitted"
};
try {
if (!timingSafeEqual(unsignedCookieToken, await verifySignedToken(signedCookieToken, config.secret))) return {
isValid: false,
reason: "Cookie integrity check failed"
};
if (!timingSafeEqual(submittedToken, unsignedCookieToken)) return {
isValid: false,
reason: "Token mismatch"
};
return { isValid: true };
} catch (error) {
if (error instanceof Error) return {
isValid: false,
reason: error.message
};
return {
isValid: false,
reason: "Token validation failed"
};
}
}
async function validateRequest(request, config, getTokenFromRequest) {
switch (config.strategy) {
case "signed-token": return await validateSignedToken(request, config, getTokenFromRequest);
case "origin-check": return validateOrigin(request, config);
case "double-submit": return await validateDoubleSubmit(request, config, getTokenFromRequest);
case "signed-double-submit": return await validateSignedDoubleSubmit(request, config, getTokenFromRequest);
case "hybrid": {
const originResult = validateOrigin(request, config);
if (!originResult.isValid) return originResult;
return await validateSignedToken(request, config, getTokenFromRequest);
}
default: return {
isValid: false,
reason: "Invalid strategy"
};
}
}
//#endregion
export { validateDoubleSubmit, validateOrigin, validateRequest, validateSignedDoubleSubmit, validateSignedToken };
//# sourceMappingURL=validation.mjs.map