I have set up the csurf
node module to add CSRF protection to my ExpressJS application. The frontend is a ReactJS single page application. The problem is that I'm constantly getting this error with every POST request: ForbiddenError: invalid csrf token
. I found out that the CSRF token somehow changes in between requests and doesn't persist. How can I resolve this issue? Here's my Express server code:
import * as dotenv from "dotenv";
import express, { Express } from 'express';
import morgan from 'morgan';
import helmet from 'helmet';
import cors from 'cors';
import cookieSession from 'cookie-session';
import cookieParser from 'cookie-parser';
import csrf from 'csurf';
import passport from 'passport';
import fs from 'fs';
import config from '../config.json';
dotenv.config({ path: `.env.${config.NODE_ENV}` });
const app: Express = express();
/************************************************************************************
* Basic Express Middlewares
***********************************************************************************/
app.set('json spaces', 4);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
const useSecureCookies: boolean = (process.env.NODE_ENV !== 'development' && process.env.NODE_ENV !== 'test');
app.use(cookieSession({
name: 'myapp',
keys: [process.env.COOKIE_SECRET],
secure: useSecureCookies,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}));
// Handle logs in console during development
if (process.env.NODE_ENV === 'development' || config.NODE_ENV === 'development') {
app.use(morgan('dev'));
app.use(cors());
}
// Handle security and origin in production
if (process.env.NODE_ENV === 'production' || config.NODE_ENV === 'production') {
app.use(helmet());
}
/***********************************
* CSRF Protection *
***********************************/
app.use(csrf({
cookie: {
secure: useSecureCookies,
httpOnly: true
}
}));
// Add the CSRF token to every request
// Source: https://stackoverflow.com/a/33060074/2430657
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
res.cookie("X-CSRF-TOKEN", req.csrfToken());
next();
});
/***********************************
* Authentication *
***********************************/
app.use(passport.initialize());
app.use(passport.session());
...
export default app;
I've created a new route in app/routes/session.ts
:
import { Router } from 'express';
import passport from 'passport';
import { BASE_ENDPOINT } from "../../constants/endpoint";
import User from '../../db/models/user';
export const router: Router = Router();
router.get(`${BASE_ENDPOINT}/sessions/sign_in`, async (req, res) => {
res.status(200).send({
token: req.csrfToken(),
session: req.session?.csrfSecret
});
});
router.post(`${BASE_ENDPOINT}/sessions/sign_in`,
passport.authenticate('local'),
async (req, res) => {
res.status(200).send({
user: (req.user as User).toJSON()
});
}
);
I'm using a Ruby console to make GET and POST requests to test my server's API:
- I make a GET request to /sessions/sign_in to get the CSRF token
- I make a POST request to /sessions/sign_in with the user's email and password. I've tried including a
_csrf
field with the token in the POST body and including anX-CSRF-TOKEN
header with the token, but none of have worked.
Any ideas as to how I could persist the token would be greatly appreciated.