1

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:

  1. I make a GET request to /sessions/sign_in to get the CSRF token
  2. 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 an X-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.

Alexander
  • 3,959
  • 2
  • 31
  • 58

2 Answers2

2

I figured it out. It's because my browser and server weren't sending cookies. Here's what I did to fix it:

Client

  1. Add the withCredentials: true option to my instance of axios:
const axiosInstance = axios.create({
  baseURL: process.env.SERVER_API_BASE_URL,
  withCredentials: true
});

Server

  1. Update the CORS to send credentials to the client. In server.ts:
import cors from 'cors';

...

app.use(cors({
  credentials: true,
  origin: "http://localhost:8080" // The host name of the client
}));

...

Now all I do is send the CSRF token from the server to the client then include it in my requests.

Alexander
  • 3,959
  • 2
  • 31
  • 58
0

Just to complement, since I struggled with this a bit:

When setting cors on express, it's important to explicitly set the origin value (origin: "http://localhost:8080") rather than (origin: "*"). Here is the explanation:

Note: When responding to a credentialed requests request, the server must specify an origin in the value of the Access-Control-Allow-Origin header, instead of specifying the "*" wildcard.

https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS

Arthur Borba
  • 403
  • 3
  • 16