typescript 92 lines · 3 tabs

CSRF protection with double-submit cookie

Shared by codesnips Jan 2026
3 tabs
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;
3 files · typescript Explain with highlit

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

ruby
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

jwt authentication api
by Kai Nakamura 2 tabs
typescript
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

typescript reliability retry
by codesnips 2 tabs
bash
#!/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

secrets-management vault environment-variables
by Kai Nakamura 1 tab
typescript
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

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
typescript
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)

security node jwt
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

CSRF protection with double-submit cookie — share card
Link copied