CSRF in Node.js for SPA/Next.js: How csurf Works and Best Practices

I wanted a clear mental model for CSRF in modern React/Next.js apps, especially when backend APIs are in Node.js.

This write-up answers three questions:

  1. How csurf actually works.
  2. Which assumptions are correct (and which need correction).
  3. What best practice looks like for SPA/Next.js today.

First: when CSRF matters

CSRF is primarily a risk when authentication is cookie-based, because browsers automatically attach cookies to requests.

If your API uses only bearer tokens in the Authorization header (and does not rely on auth cookies), CSRF risk is much lower. In that model, XSS is usually the bigger concern.

How csurf works in Express

Important context: the original expressjs/csurf repository is archived and no longer actively maintained. The behavior is still useful to understand, because many projects use the same model.

csurf does not just create a random one-off token per request. Instead:

  • It stores a CSRF secret (in session by default, or cookie if cookie: true).
  • req.csrfToken() creates a token derived from that secret.
  • On state-changing requests, middleware validates submitted token against that secret.
  • The token can be provided in body/query or headers like X-CSRF-Token.

So yes, it can use cookies, but when cookie: true, it stores the secret in cookie. You still pass the request token in header/body/query.

Double-submit vs csurf internals

These two ideas are closely related but not identical.

  • Classic double-submit cookie: server checks whether CSRF value in cookie matches CSRF value sent in header/body.
  • csurf with cookie: true: cookie stores a CSRF secret, client sends a token generated from that secret, and middleware validates token against the secret.

In practice, the client flow looks similar (send token on mutating requests), but server-side validation logic is different.

CSRF process in SPA/Next.js

  1. Server generates CSRF secret/token and exposes a CSRF token to the client.
  2. Client keeps the token in memory or reads it from a CSRF cookie when needed.
  3. Client attaches token to POST/PUT/PATCH/DELETE in a custom header (for example X-CSRF-Token).
  4. Server validates the submitted token against the stored secret/cookie value.
  5. If validation fails, server returns 403.
  6. The same token can be reused across parallel mutating requests in the same session context.
  7. Token rotation is optional per request; most setups rotate on auth/session events (login, logout, renewal) rather than every write call.

Best-practice pattern for SPA/Next.js (cookie auth)

For cookie-authenticated web apps, use layered defense:

  1. Cookie hardening for session/refresh cookies:
    • HttpOnly
    • Secure
    • SameSite=Lax or Strict where possible
  2. CSRF token check on state-changing routes.
  3. Origin/Referer validation for sensitive endpoints.
  4. Never mutate state via GET.

For SPA APIs, use cookie-to-header (double-submit style):

  • Server sets CSRF cookie (readable by JS if client must echo it).
  • Client sends the same token in X-CSRF-Token.
  • Server verifies cookie value and header value (and signature/session binding if used).

Next.js practical guidance

  • If using NextAuth or another cookie session layer, keep CSRF in place for custom state-changing endpoints.
  • Centralize protection:
    • One API client wrapper that auto-attaches CSRF header.
    • One server-side guard/middleware for all POST/PUT/PATCH/DELETE.
  • Do not call raw fetch() everywhere for mutating requests.

Should CSRF token be HttpOnly?

Usually no, for cookie-to-header SPA pattern.

Why: the browser JavaScript must read the CSRF token value to send it in header. That means this specific CSRF cookie is intentionally readable by JS.

This is different from session/refresh cookies, which should stay HttpOnly.

Token lifecycle recommendations

  • Generate CSRF token when app shell/session is established.
  • Reuse token for concurrent requests in same session.
  • Rotate on major auth events (login/logout/session renewal) or periodically.
  • On CSRF validation failure, return 403 and require token refresh/retry flow.

2026 default stack I use

  • Cookie-auth session/refresh with HttpOnly + Secure + SameSite.
  • CSRF token via cookie-to-header on mutating routes.
  • Origin check + strict CORS.
  • No state-changing GET.
  • XSS hardening (CSP, output encoding), because XSS can bypass most CSRF defenses.

That combination is simple, scalable, and robust for most React/Next.js apps.

References