In my previous blog post I wrote about a method that could be used to prevent CSRF attacks. As promised, here I am with another write up about another pattern that could be used to prevent CSRF attacks.

The Double Submit Cookie pattern uses cookies to prevent CSRF and does not require the server to store token on its side. When a client application authenticates, the server will generate a cookie that contains a token, which will be sent to the client along with the response to the authentication call. Then the client would get the token from the cookie and set it within a hidden value in a form, so that every request would contain the token.

Now at this stage, a request coming into the server contains the token in two separate fields. That is in the cookie and in the body of the request where the form data is set.

The server would extract these two token values and compare before processing the request. If the two token values match, the server would process the request and send the corresponding response to the client or else it would generate an error saying that the tokens do not match.

Let's have a look at a simple implementation using NodeJS.

Once the user logs into the application using the username and password, the frontend sends request to the /login endpoint where the inputs are validated and then the csrf token along with the session identifier is generated and set to the cookies. Then a redirect to the form page is sent to the frontend.

When the form page is loaded, it extracts the csrf token from the cookie and sets it in a hidden field in the form so that it is injects to all the outgoing requests. The server side on receiving a request, validates it by comparing the value in the request body and that of the cookie and if the values match, it sends a success message or else sends an error message.

const express = require('express'),
  bodyParser = require('body-parser'),
  cookieParser = require('cookie-parser'),
  randomBytes = require('random-bytes'),
  app = express(),
  PORT = 9090,
  USERNAME = "demo",
  PASSWORD = "pass123$";

app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(express.static('public'));
app.listen(PORT, err => {
  if (err) {
    console.error(`ERROR: Can not start server on ${PORT}`);
    return;
  }
  console.log(`SUCCESS: Server started on port ${PORT}`);
});

app.get('/', (req, res) => {
  if (req.cookies['csrf-token'] && req.cookies['session-id']) {
    res.redirect('/form.html');
    return;
  }
  res.redirect('/login.html');
});

app.post('/login', (req, res) => {
  let username = req.body.username;
  let password = req.body.password;
  if (username === undefined || username === "") {
    res.status(400).json({ success: false, message: "Username undefined" });
    return;
  }
  if (password === undefined || password === "") {
    res.status(400).json({ success: false, message: "Password undefined" });
    return;
  }
  if (username === USERNAME && password === PASSWORD) {
    let session_id = Buffer.from(randomBytes.sync(32)).toString('base64');
    let csrf_token = Buffer.from(randomBytes.sync(32)).toString('base64');
    res.setHeader('Set-Cookie', [
      `session-id=${session_id}`,
      `csrf-token=${csrf_token}`,
      `time=${Date.now()}`
    ]);
    res.sendFile('public/form.html', { root: __dirname });
  } else {
    res.status(405).json({ success: false, message: "Unauthorized user" });
    res.redirect('/');
  }
});

app.post('/post', (req, res) => {
  let session_id = req.cookies['session-id'];
  let csrf_token = req.cookies['csrf-token'];
  if (session_id) {
    if (csrf_token === req.body.csrf_token) {
      res.status(200).json({ success: true });
    } else {
      res.status(400).json({ success: false });
    }
  } else {
    res.sendFile('public/login.html', { root: __dirname });
  }
});

Repository: https://github.com/ntbandara3/double-submit-cookies-pattern