@csrf-armor/nextjs
Version:
CSRF protection middleware for Next.js applications
97 lines (96 loc) • 4.04 kB
JavaScript
//#region src/adapter.ts
var NextjsAdapter = class {
constructor() {
this.parsedBodyCache = /* @__PURE__ */ new WeakMap();
this.getTokenFromRequest = this.getTokenFromRequest.bind(this);
}
extractRequest(req) {
const cookies = /* @__PURE__ */ new Map();
for (const { name, value } of req.cookies.getAll()) cookies.set(name, value);
return {
method: req.method,
url: req.url,
headers: req.headers,
cookies,
body: req
};
}
applyResponse(res, csrfResponse) {
if (csrfResponse.headers instanceof Map) for (const [key, value] of csrfResponse.headers) res.headers.set(key, value);
else for (const [key, value] of Object.entries(csrfResponse.headers)) res.headers.set(key, String(value));
if (csrfResponse.cookies instanceof Map) for (const [name, cookieData] of csrfResponse.cookies) {
const { value, options } = cookieData;
res.cookies.set(name, value, this.adaptCookieOptions(options));
}
else for (const [name, cookieData] of Object.entries(csrfResponse.cookies)) {
const { value, options } = cookieData;
res.cookies.set(name, value, this.adaptCookieOptions(options));
}
return res;
}
async getTokenFromRequest(request, config) {
const headers = request.headers;
const nextRequest = request.body;
const headerValue = headers.get(config.token.headerName.toLowerCase());
if (headerValue) return headerValue;
const clientCookieValue = nextRequest.cookies?.get(config.cookie.name.toLowerCase())?.value;
if (clientCookieValue) return clientCookieValue;
let parsedBody;
if (this.parsedBodyCache.has(nextRequest)) parsedBody = this.parsedBodyCache.get(nextRequest);
else {
const contentType = headers.get("content-type") ?? "text/plain";
try {
if (nextRequest.bodyUsed) {
console.warn("Request body was already consumed externally. CSRF token might not be extractable from body.");
parsedBody = null;
} else if (contentType.startsWith("application/x-www-form-urlencoded") || contentType.startsWith("multipart/form-data")) parsedBody = await nextRequest.formData();
else if (contentType === "application/json" || contentType === "application/ld+json") parsedBody = await nextRequest.json();
else if (contentType.startsWith("text/plain")) parsedBody = await nextRequest.text();
else parsedBody = null;
this.parsedBodyCache.set(nextRequest, parsedBody);
} catch (error) {
console.warn("Failed to parse request body for CSRF token extraction", error);
this.parsedBodyCache.set(nextRequest, null);
parsedBody = null;
}
}
if (parsedBody instanceof FormData) {
for (const [key, value] of parsedBody.entries()) if (key === config.token.fieldName) return value.toString();
} else if (parsedBody && typeof parsedBody === "object") {
const jsonVal = parsedBody[config.token.fieldName];
if (typeof jsonVal === "string") return jsonVal;
if (Array.isArray(parsedBody) && parsedBody.length > 0) return this.extractTokenFromServerActionArgs(parsedBody, config);
} else if (typeof parsedBody === "string") try {
const tokenValue = new URLSearchParams(parsedBody).get(config.token.fieldName);
if (tokenValue) return tokenValue;
} catch (error) {
console.warn("Failed to parse string body as URL-encoded form data", error);
}
}
extractTokenFromServerActionArgs(args, config) {
const firstArg = args[0];
if (typeof firstArg === "string") return firstArg;
if (firstArg && typeof firstArg === "object") {
const token = firstArg[config.token.fieldName];
if (typeof token === "string") return token;
}
for (const arg of args) if (arg && typeof arg === "object") {
const token = arg[config.token.fieldName];
if (typeof token === "string") return token;
}
}
adaptCookieOptions(options) {
if (!options) return {};
return {
secure: options.secure,
httpOnly: options.httpOnly,
sameSite: options.sameSite,
path: options.path,
domain: options.domain,
maxAge: options.maxAge
};
}
};
//#endregion
export { NextjsAdapter };
//# sourceMappingURL=adapter.js.map