CSRF Double-Submit Token
Issue HMAC-bound CSRF tokens and verify that the header, cookie and session agree before accepting a state change.
Code
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
export interface CsrfCheckInput {
headerToken?: string;
cookieToken?: string;
sessionId: string;
}
export function createCsrfToken(sessionId: string, secret: string): string {
if (!sessionId || !secret) throw new TypeError("sessionId and secret are required");
const nonce = randomBytes(18).toString("base64url");
const signature = createHmac("sha256", secret)
.update(sessionId + "." + nonce)
.digest("base64url");
return nonce + "." + signature;
}
export function verifyCsrfToken(input: CsrfCheckInput, secret: string): boolean {
const { headerToken, cookieToken, sessionId } = input;
if (!headerToken || !cookieToken || !sessionId) return false;
const header = Buffer.from(headerToken);
const cookie = Buffer.from(cookieToken);
if (header.length !== cookie.length || !timingSafeEqual(header, cookie)) return false;
const separator = headerToken.lastIndexOf(".");
if (separator <= 0) return false;
const nonce = headerToken.slice(0, separator);
const signature = headerToken.slice(separator + 1);
const expected = createHmac("sha256", secret)
.update(sessionId + "." + nonce)
.digest("base64url");
const provided = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
return provided.length === expectedBuffer.length
&& timingSafeEqual(provided, expectedBuffer);
}
export function csrfCookie(token: string, secure = true): string {
const flags = ["Path=/", "SameSite=Lax", secure ? "Secure" : ""].filter(Boolean);
return "csrf_token=" + encodeURIComponent(token) + "; " + flags.join("; ");
}Report a problem with this exampleOpens a pre-filled issue on GitHub.