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:
- How
csurfactually works. - Which assumptions are correct (and which need correction).
- 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.
csurfwithcookie: 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
- Server generates CSRF secret/token and exposes a CSRF token to the client.
- Client keeps the token in memory or reads it from a CSRF cookie when needed.
- Client attaches token to
POST/PUT/PATCH/DELETEin a custom header (for exampleX-CSRF-Token). - Server validates the submitted token against the stored secret/cookie value.
- If validation fails, server returns
403. - The same token can be reused across parallel mutating requests in the same session context.
- 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:
- Cookie hardening for session/refresh cookies:
HttpOnlySecureSameSite=LaxorStrictwhere possible
- CSRF token check on state-changing routes.
- Origin/Referer validation for sensitive endpoints.
- 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
403and 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.