e bloat.
- The architecture scales horizontally since Redis acts as a centralized, stateless validation store.
Core Solution
The implementation follows a strict double-submit cookie flow backed by Redis for stateful validation. Below is the complete technical stack, preserving all original implementation details.
Backend Setup
Install required packages:
npm install express cors cookie-parser
Example Express setup:
// app.js
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import csrfRoutes from "./routes/csrf.routes.js";
import postRoutes from "./routes/post.routes.js";
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
origin: "http://localhost:5173",
credentials: true,
})
);
app.use("/api", csrfRoutes);
app.use("/api", postRoutes);
export default app;
credentials: true is important because we are using cookies.
Create CSRF Token
// controllers/csrf.controller.js
import crypto from "crypto";
import { Redis } from "../config/redis.js";
export const getCsrfToken = async (req, res) => {
const csrfToken = crypto.randomBytes(32).toString("hex");
// Store token in Redis for 30 minutes
await Redis.set(`csrf:${req.user.id}`, csrfToken, "EX", 60 * 30);
res.cookie("csrfToken", csrfToken, {
httpOnly: false,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 30 * 60 * 1000,
});
res.status(200).json({
csrfToken,
});
};
For auth cookies, use httpOnly: true.
But for this CSRF token, we use httpOnly: false because the frontend needs to send the token in a request header.
CSRF Middleware
// middlewares/csrf.middleware.js
import { Redis } from "../config/redis.js";
const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];
export const csrfProtection = async (req, res, next) => {
if (SAFE_METHODS.includes(req.method)) {
return next();
}
const tokenFromHeader = req.headers["x-csrf-token"];
const tokenFromCookie = req.cookies.csrfToken;
if (!tokenFromHeader || !tokenFromCookie) {
return res.status(403).json({
error: "CSRF token missing",
});
}
if (tokenFromHeader !== tokenFromCookie) {
return res.status(403).json({
error: "CSRF token mismatch",
});
}
const storedToken = await Redis.get(`csrf:${req.user.id}`);
if (!storedToken || storedToken !== tokenFromHeader) {
return res.status(403).json({
error: "Invalid CSRF token",
});
}
next();
};
This middleware checks:
1. Token from header
2. Token from cookie
3. Token from Redis
If all match, the request is allowed.
CSRF Route
// routes/csrf.routes.js
import express from "express";
import { getCsrfToken } from "../controllers/csrf.controller.js";
import { authMiddleware } from "../middlewares/auth.middleware.js";
const router = express.Router();
router.get("/csrf-token", authMiddleware, getCsrfToken);
export default router;
The endpoint is:
GET /api/csrf-token
Use CSRF Middleware on Unsafe Routes
// routes/post.routes.js
import express from "express";
import { authMiddleware } from "../middlewares/auth.middleware.js";
import { csrfProtection } from "../middlewares/csrf.middleware.js";
const router = express.Router();
router.post("/post", authMiddleware, csrfProtection, createPost);
router.put("/post/:id", authMiddleware, csrfProtection, updatePost);
router.delete("/post/:id", authMiddleware, csrfProtection, deletePost);
export default router;
Use CSRF protection mainly on:
POST
PUT
PATCH
DELETE
You usually do not need it on normal GET routes.
Frontend Setup
Create an Axios instance:
// src/api/api.js
import axios from "axios";
export const api = axios.create({
baseURL: "http://localhost:5000/api",
withCredentials: true,
});
withCredentials: true sends cookies with requests.
Fetch CSRF Token
// src/api/csrf.js
import { api } from "./api";
let csrfToken = null;
export const fetchCsrfToken = async () => {
const res = await api.get("/csrf-token");
csrfToken = res.data.csrfToken;
return csrfToken;
};
export const getCsrfToken = () => csrfToken;
Call this after login:
await fetchCsrfToken();
Send CSRF Token in Requests
// src/api/post.js
import { api } from "./api";
import { getCsrfToken } from "./csrf";
export const createPost = async (postData) => {
const csrfToken = getCsrfToken();
const res = await api.post("/post", postData, {
headers: {
"X-CSRF-Token": csrfToken,
},
});
return res.data;
};
Now the request sends:
Cookie: accessToken=...
Cookie: csrfToken=...
X-CSRF-Token: your_csrf_token
The backend verifies the token before allowing the request.
Pitfall Guide
- [Misconfigured
httpOnly Flag]: Setting httpOnly: true on the CSRF cookie prevents JavaScript from reading it, breaking the double-submit flow. The frontend must access the cookie to attach it to the X-CSRF-Token header.
- [Skipping
credentials: true]: Both CORS configuration (credentials: true) and Axios (withCredentials: true) are mandatory. Omitting either breaks cross-origin cookie transmission, causing silent authentication failures.
- [Over-Applying to Safe Methods]: Applying CSRF validation to
GET, HEAD, or OPTIONS violates REST conventions, breaks browser caching, and interferes with link previews or prefetching. Restrict middleware to state-changing methods.
- [Token Expiration Mismatch]: Redis TTL (
EX 60 * 30) and cookie maxAge must align precisely. Drift causes either premature 403 errors (Redis expires first) or security gaps (cookie outlives server state).
- [Incomplete Triple Verification]: Validating only header vs. cookie without checking Redis enables replay attacks. If an attacker intercepts a cookie, they can forge requests unless the server-side store validates freshness and ownership.
- [Ignoring Production
secure Flag]: Failing to set secure: true in production exposes the CSRF token to man-in-the-middle attacks over unencrypted HTTP. Always conditionally enforce TLS-bound cookies.
- [Static Token Caching]: Storing the CSRF token indefinitely in frontend memory or
localStorage without refresh logic causes failures after session rotation or token expiration. Implement automatic re-fetch on 403 responses.
Deliverables
π Architecture Blueprint
- Data flow diagram mapping React β Express β Redis validation pipeline
- Component dependency matrix (CORS, cookie-parser, crypto, Axios interceptors)
- State lifecycle visualization (Token generation β Cookie attachment β Header injection β Triple verification β TTL cleanup)
β
Deployment & Security Checklist