import express from 'express';
import cookieParser from 'cookie-parser';
import { issueCsrfToken, csrfProtection } from './csrf';
import { transfersRouter } from './routes/transfers';
const app = express();
app.use(express.json());
app.use(cookieParser());
// Refresh the token cookie on every response.
app.use(issueCsrfToken);
// Guard all mutating routes registered after this line.
app.use(csrfProtection);
app.use('/api/transfers', transfersRouter);
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
res.status(500).json({ error: 'internal_error' });
});
export default app;
import { randomBytes, timingSafeEqual } from 'crypto';
import { Request, Response, NextFunction } from 'express';
const COOKIE_NAME = 'csrf_token';
const HEADER_NAME = 'x-csrf-token';
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
export function issueCsrfToken(req: Request, res: Response, next: NextFunction) {
if (!req.cookies[COOKIE_NAME]) {
const token = randomBytes(32).toString('hex');
res.cookie(COOKIE_NAME, token, {
httpOnly: false, // front-end must read this to echo it back
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
path: '/',
});
}
next();
}
export function csrfProtection(req: Request, res: Response, next: NextFunction) {
if (SAFE_METHODS.has(req.method)) return next();
const cookieToken = req.cookies[COOKIE_NAME];
const headerToken = req.get(HEADER_NAME);
if (!cookieToken || !headerToken || cookieToken.length !== headerToken.length) {
return res.status(403).json({ error: 'csrf_token_mismatch' });
}
const a = Buffer.from(cookieToken);
const b = Buffer.from(headerToken);
if (!timingSafeEqual(a, b)) {
return res.status(403).json({ error: 'csrf_token_mismatch' });
}
next();
}
function readCookie(name: string): string | null {
const match = document.cookie
.split('; ')
.find((row) => row.startsWith(`${name}=`));
return match ? decodeURIComponent(match.split('=')[1]) : null;
}
export async function apiFetch(input: string, init: RequestInit = {}): Promise<Response> {
const token = readCookie('csrf_token');
const headers = new Headers(init.headers);
if (token) headers.set('x-csrf-token', token);
if (init.body && !headers.has('Content-Type')) {
headers.set('Content-Type', 'application/json');
}
const res = await fetch(input, {
...init,
headers,
credentials: 'include', // send the csrf_token cookie alongside
});
if (res.status === 403) {
const body = await res.clone().json().catch(() => ({}));
if (body?.error === 'csrf_token_mismatch') {
throw new Error('CSRF validation failed; reload to obtain a fresh token.');
}
}
return res;
}
The double-submit cookie pattern defends against Cross-Site Request Forgery without requiring server-side session state to hold the token. The idea is that a random CSRF token is placed in a cookie AND echoed back by the client in a request header; on unsafe requests the server checks that the two match. Because a cross-origin attacker can force a browser to send cookies but cannot read them (thanks to the same-origin policy) nor set a custom header on a cross-site request, only a genuine same-site script can produce a matching header, so forged requests fail the comparison.
In csrf.ts, issueCsrfToken generates 32 bytes of entropy with randomBytes and writes it into a cookie via res.cookie. The cookie is deliberately NOT httpOnly because front-end JavaScript must read it to copy the value into the header — this is safe under the double-submit model since the token is not itself a secret credential. sameSite: 'lax' and secure add defense in depth. The csrfProtection middleware skips safe methods (GET, HEAD, OPTIONS) that RFC 7231 defines as non-mutating, then compares the cookie value against the x-csrf-token header using timingSafeEqual to avoid leaking information through comparison timing. Unequal lengths short-circuit before the constant-time check, which requires equal-length buffers.
In app.ts, issueCsrfToken runs early so every response refreshes the cookie, and csrfProtection guards the mutating routes. Order matters: cookieParser must run first so req.cookies is populated, and csrfProtection must sit before the route handlers it protects. A rejected request returns 403 with a stable error code the client can key off of.
In apiClient.ts, readCookie parses document.cookie to recover the token the server set, and apiFetch injects it as the x-csrf-token header on every request while sending credentials: 'include' so the cookie travels too. This closes the loop: the same token exists in both places only for legitimate same-origin calls.
The main trade-off is that the pattern trusts the browser's origin isolation rather than a server secret, so it pairs poorly with subdomains that share cookies unless the token is bound per-host; a signed or HMAC-bound token variant hardens against that. It remains a lightweight, stateless choice for APIs that already rely on cookie auth.
Related snips
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
#!/usr/bin/env bash
set -euo pipefail
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN="${VAULT_TOKEN:?missing VAULT_TOKEN}"
Secrets management with environment isolation and Vault
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";
const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";
JWT access + refresh token rotation (conceptual)
Share this code
Here's the card — post it anywhere.