remix-utils
Version:
This package contains simple utility functions to use with [React Router](https://reactrouter.com/).
123 lines • 4.97 kB
JavaScript
import { sha256 } from "@oslojs/crypto/sha2";
import { encodeBase64url } from "@oslojs/encoding";
import { randomString } from "../common/crypto.js";
import { getHeaders } from "./get-headers.js";
export class CSRFError extends Error {
code;
constructor(code, message) {
super(message);
this.code = code;
this.name = "CSRFError";
}
}
export class CSRF {
cookie;
formDataKey = "csrf";
secret;
constructor(options) {
this.cookie = options.cookie;
this.formDataKey = options.formDataKey ?? "csrf";
this.secret = options.secret;
}
/**
* Generates a random string in Base64URL to be used as an authenticity token
* for CSRF protection.
* @param bytes The number of bytes used to generate the token
* @returns A random string in Base64URL
*/
generate(bytes = 32) {
let token = randomString(bytes);
if (!this.secret)
return token;
let signature = this.sign(token);
return [token, signature].join(".");
}
/**
* Get the existing token from the cookie or generate a new one if it doesn't
* exist.
* @param requestOrHeaders A request or headers object from which we can
* get the cookie to get the existing token.
* @param bytes The number of bytes used to generate the token.
* @returns The existing token if it exists in the cookie, otherwise a new
* token.
*/
async getToken(requestOrHeaders = new Headers(), bytes = 32) {
let headers = getHeaders(requestOrHeaders);
let existingToken = await this.cookie.parse(headers.get("cookie"));
let token = typeof existingToken === "string" ? existingToken : this.generate(bytes);
return token;
}
/**
* Generates a token and serialize it into the cookie.
* @param requestOrHeaders A request or headers object from which we can
* get the cookie to get the existing token.
* @param bytes The number of bytes used to generate the token
* @returns A tuple with the token and the string to send in Set-Cookie
* If there's already a csrf value in the cookie then the token will
* be the same and the cookie will be null.
* @example
* let [token, cookie] = await csrf.commitToken(request);
* return json({ token }, {
* headers: { "set-cookie": cookie }
* })
*/
async commitToken(requestOrHeaders = new Headers(), bytes = 32) {
let headers = getHeaders(requestOrHeaders);
let existingToken = await this.cookie.parse(headers.get("cookie"));
let token = typeof existingToken === "string" ? existingToken : this.generate(bytes);
let cookie = existingToken ? null : await this.cookie.serialize(token);
return [token, cookie];
}
async validate(data, headers) {
if (data instanceof Request && data.bodyUsed) {
throw new Error("The body of the request was read before calling CSRF#verify. Ensure you clone it before reading it.");
}
let formData = await this.readBody(data);
let cookie = await this.parseCookie(data, headers);
// if the session doesn't have a csrf token, throw an error
if (cookie === null) {
throw new CSRFError("missing_token_in_cookie", "Can't find CSRF token in cookie.");
}
if (typeof cookie !== "string") {
throw new CSRFError("invalid_token_in_cookie", "Invalid CSRF token in cookie.");
}
if (this.verifySignature(cookie) === false) {
throw new CSRFError("tampered_token_in_cookie", "Tampered CSRF token in cookie.");
}
// if the body doesn't have a csrf token, throw an error
if (!formData.get(this.formDataKey)) {
throw new CSRFError("missing_token_in_body", "Can't find CSRF token in body.");
}
// if the body csrf token doesn't match the session csrf token, throw an
// error
if (formData.get(this.formDataKey) !== cookie) {
throw new CSRFError("mismatched_token", "Can't verify CSRF token authenticity.");
}
}
async readBody(data) {
if (data instanceof FormData)
return data;
return await data.clone().formData();
}
parseCookie(data, headers) {
let _headers = data instanceof Request ? data.headers : headers;
if (!_headers)
return null;
return this.cookie.parse(_headers.get("cookie"));
}
sign(token) {
if (!this.secret)
return token;
return encodeBase64url(sha256(new TextEncoder().encode(token)));
}
verifySignature(token) {
if (!this.secret)
return true;
let [value, signature] = token.split(".");
if (!value)
return false;
let expectedSignature = this.sign(value);
return signature === expectedSignature;
}
}
//# sourceMappingURL=csrf.js.map