abiball/assets/controllers/csrf_protection_controller.js

118 lines
3.2 KiB
JavaScript
Raw Permalink Normal View History

const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
const tokenCheck = /^[-_\/+a-zA-Z0-9]{24,}$/;
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
document.addEventListener(
"submit",
function (event) {
generateCsrfToken(event.target);
},
true,
);
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
document.addEventListener("turbo:submit-start", function (event) {
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
Object.keys(h).map(function (k) {
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
});
});
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
document.addEventListener("turbo:submit-end", function (event) {
removeCsrfToken(event.detail.formSubmission.formElement);
});
export function generateCsrfToken(formElement) {
const csrfField = formElement.querySelector(
'input[data-controller="csrf-protection"], input[name="_csrf_token"]',
);
if (!csrfField) {
return;
}
let csrfCookie = csrfField.getAttribute("data-csrf-protection-cookie-value");
let csrfToken = csrfField.value;
if (!csrfCookie && nameCheck.test(csrfToken)) {
csrfField.setAttribute(
"data-csrf-protection-cookie-value",
(csrfCookie = csrfToken),
);
csrfField.defaultValue = csrfToken = btoa(
String.fromCharCode.apply(
null,
(window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18)),
),
);
csrfField.dispatchEvent(new Event("change", { bubbles: true }));
}
if (csrfCookie && tokenCheck.test(csrfToken)) {
const cookie =
csrfCookie +
"_" +
csrfToken +
"=" +
csrfCookie +
"; path=/; samesite=strict";
document.cookie =
window.location.protocol === "https:"
? "__Host-" + cookie + "; secure"
: cookie;
}
}
export function generateCsrfHeaders(formElement) {
const headers = {};
const csrfField = formElement.querySelector(
'input[data-controller="csrf-protection"], input[name="_csrf_token"]',
);
if (!csrfField) {
return headers;
}
const csrfCookie = csrfField.getAttribute(
"data-csrf-protection-cookie-value",
);
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
headers[csrfCookie] = csrfField.value;
}
return headers;
}
export function removeCsrfToken(formElement) {
const csrfField = formElement.querySelector(
'input[data-controller="csrf-protection"], input[name="_csrf_token"]',
);
if (!csrfField) {
return;
}
const csrfCookie = csrfField.getAttribute(
"data-csrf-protection-cookie-value",
);
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
const cookie =
csrfCookie +
"_" +
csrfField.value +
"=0; path=/; samesite=strict; max-age=0";
document.cookie =
window.location.protocol === "https:"
? "__Host-" + cookie + "; secure"
: cookie;
}
}
/* stimulusFetch: 'lazy' */
export default "csrf-protection-controller";