VOOZH about

URL: https://dev.to/pratik-k-ghosh/csrf-protection-in-react-express-simple-explanation-with-code-14oo

⇱ CSRF Protection in React + Express: Simple Explanation with Code - DEV Community


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:

  1. Backend creates a CSRF token.
  2. Backend stores it in Redis.
  3. Backend sends the token to React.
  4. React sends the token back in a custom header.
  5. 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

  1. Use secure cookie-based authentication.
  2. Create GET /api/csrf-token.
  3. Generate a random CSRF token.
  4. Store the token in Redis.
  5. Send the token to React.
  6. Send the token back in X-CSRF-Token header.
  7. Verify header token, cookie token, and Redis token in middleware.
  8. Apply middleware on POST, PUT, PATCH, and DELETE routes.

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.