CSRF Double-Submit Token

Issue HMAC-bound CSRF tokens and verify that the header, cookie and session agree before accepting a state change.

TypescriptAuth Jwt

Code

snippet
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("; ");
}