CSRF Protection in React + Express
When we use cookie-based authentication, we should understand CSRF protection.
CSRF means Cross-Site Request Forgery.
In simple words, CSRF is an attack where a malicious website tricks your browser into sending a request to a website where you are already logged in.
Example:
You are logged in to:
https://myapp.com
Then you visit:
https://evil-site.com
The evil website may try to send a request like:
POST https://myapp.com/api/delete-post
Because browsers automatically send cookies, your login cookie may also be sent with that request.
So the backend may think the request came from you.
That is why CSRF protection is needed.
CSRF protection matters because cookie-based authentication can be misused if a malicious website tricks the browser into sending unwanted requests. Even if your auth cookies are secure and httpOnly, the browser may still send them automatically. A CSRF token adds an extra verification layer so the backend can trust that the request came from your own frontend.
Basic Idea
If your backend uses cookies for auth, then the browser sends cookies automatically.
So we add one extra check: CSRF Token
The flow is simple:
- Backend creates a CSRF token.
- Backend stores it in Redis.
- Backend sends the token to React.
- React sends the token back in a custom header.
- Backend verifies the token before allowing POST, PUT, PATCH, or DELETE requests.
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.
Final Steps to Make Your App CSRF Safe
- Use secure cookie-based authentication.
- Create
GET /api/csrf-token. - Generate a random CSRF token.
- Store the token in Redis.
- Send the token to React.
- Send the token back in
X-CSRF-Tokenheader. - Verify header token, cookie token, and Redis token in middleware.
- Apply middleware on
POST,PUT,PATCH, andDELETEroutes.
Final Thought
Authentication proves who the user is.
CSRF protection helps prove the request came from your own frontend.
So if you are using cookies for authentication in a React + Express app, adding CSRF protection is a good security improvement.
Top comments (0)