UNPKG

remix-utils

Version:

This package contains simple utility functions to use with [React Router](https://reactrouter.com/).

123 lines 4.97 kB
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