Protecting Universal React Applications Against CSRF With Express Sessions

Alexander Paterson
|
Posted about 6 years ago
|
18 minutes
Protecting Universal React Applications Against CSRF With Express Sessions
Moving from JWT's to user sessions allows our client server to add a layer of security

In developing an authentication system for a web application, there are generally two choices when it comes to storing authentication tokens:

  1. The browser's local storage. The browser's local storage is susceptible to XSS attacks since any javascript maliciously injected into your client application can access it. 
  2. Cookies. This leaves your application susceptible to CSRF attacks, since effectively any API call your browser makes will include these cookies and hence be authenticated by default.

However, there's a reason React developers should choose cookies over local storage, and that's universal rendering. Universal-rendering allows developers to pre-fetch data, improve page-render time, and ensure that search engines and social media sites can access important data.

If a user makes a GET request to http://myapp.com/dashboard, and they're not authenticated, I want to send back HTML for a login screen, not a dashboard they won't be allowed to access anyway. For reasons such as this, and for API authentication when pre-fetching data, our client server needs to know whether or not a user is authenticated when they first fetch your application, and for that you must use cookies.

The problem, then, is how to defend against CSRF attacks. These attacks do not involve stealing any sort of user credentials to make a malicious API call, rather, another malicious webpage instructs the user's browser to make an API call, and the user's cookies provide automatic authentication. 

The defence is to require authenticated API calls include another token: one that isn't stored in cookies. This is called a CSRF token, and it's going to get sent down in the HTML generated by our client server.

I hope the obvious question here is: how will our API know which CSRF tokens are valid if our client server sent them? We're going to use a node module called express-session, and Redis as a store, enabling cooperation between our servers.

Sessions And Stores

csrfToken cookies allow us to remove code from our application which receives/saves/clears JWT's, and instead the task of storing auth tokens will be left to our browser, obeying HTTP headers sent from the server to set and remove cookies.

Now, imagine that for every authentication token you sent to a user, you created a database entry with a set of information about that user -- an entry that was deleted when the user signed out. This is what sessions are, and the database is called the store.

Our CSRF protection will work as follows:

  1. Unauthenticated user makes API call to sign-in.
  2. Our server creates a session in the store for the user. Both our client server and our API can get and set shared information about the user in the store. 
  3. The server generates a CSRF token for the user. This is placed in the user's session in the store for future reference.
  4. The server sends back the CSRF token in the response body, and the API instructs the user's browser to store an authentication token in a cookie (simply an encrypted user_id). 
  5. The client stores the CSRF token as a global variable.
  6. Any authenticated API request includes this token as a HTTP header, and the server checks if it's valid for the user.

So at this point, we have an authentication system which requires users to sign-in every time they use our app (to set a CSRF token). Now we're going to make things such that any time a user pulls down HTML from our client server, a CSRF-token is included as a global variable already (if they're authenticated).

  1. Authenticated user (through session cookie) requests HTML from client server.
  2. The client server gets the user's CSRF token out of the store.
  3. The CSRF token is embedded in the HTML sent back from the server as a javascript block that sets a global variable.
  4. Client application sends this CSRF token with each API call.

This looks like a lot of steps, but it is actually quite easy to implement. You simply need a Redis server that both your client server and API can access.

Setting Up The Client Server

I'll start in the client server. Both our client server and API will have the same express-session configuration. Start by installing express-session(@1.15.1) and connect-redis(@3.2.0). We're simply adding a piece of express middleware as such:

//src/server/index.js

var session = require('express-session'),
    connectRedis = require('connect-redis');

var RedisStore = connectRedis(session);

//...

app.use(session({
  store: new RedisStore({
    url: process.env.REDIS_URL,
    secure: process.env.NODE_ENV=='production'
  }),
  resave: false,
  saveUninitialized: false,
  name: 'connect.sid', // Good idea to make the name somewhat cryptic
  secret: process.env.SESSION_SECRET
}));

//...

Now we have access to a new variable req.session in any requests coming to our client server. You can get and set properties on this object, and they'll persist.

The process of authenticating a user is as simple as setting req.session.user_id = user._id. express-session takes care of the rest. If req.session.user_id isn't defined, then the user wasn't authenticated!

Signing out is as simple as revoking the user's cookie and deleting the session data from our store.

Our client server might need to dispatch an action based on whether the user is authenticated or not, and if the user is authenticated, it'll need to create a CSRF token and include it in the generated HTML. Here's an example of that:

// src/server/index.js

// React application rendering
app.use((req, res, next) => {
  var store = configureStore();

  //...

  var csrfToken;
  // If no session, register that in DOM. Also unsets cookies (wow! thanks react-cookie)
  if (!req.session || !req.session.user_id) {
    store.dispatch(signoutUser);
  } else {
    if (!req.session.csrfToken) {
      csrfToken = uuid.v4();
      req.session.csrfToken = csrfToken;
    } else {
      csrfToken = req.session.csrfToken;
    }
  }

  //...


  // Use react-router to get tree to render
  match({routes: routes(store), location: req.originalUrl}, (err, redirectLocation, renderProps) => {
  var finalComponentToRender = (
    <Provider store={store}>
      <RouterContext {...renderProps}/>
    </Provider>
  );

  //...

  res.status(200).send('<!doctype html>' + '\n' + ReactDOM.renderToString(<Html component={finalComponentToRender} store={store} csrfToken={csrfToken}}/>));
};

Where Html looks a bit like this:

import React, {Component} from 'react';
import ReactDOM from 'react-dom/server';
import serialize from 'serialize-javascript';
import Helmet from 'react-helmet';

export default class Html extends Component {
  render() {
    const {component, store, csrfToken} = this.props;
    const content = component ? ReactDOM.renderToString(component) : '';

    return (
      <html lang='en-us'>
        <body>
          <div id='app' dangerouslySetInnerHTML={{__html: content}} className='scrollFix'/>
          <script dangerouslySetInnerHTML={{__html: `window.__data=${serialize(store.getState())};`}} charSet="UTF-8"/>
          <script dangerouslySetInnerHTML={{__html: `window.__csrf_token='${csrfToken}';`}} charSet="UTF-8"/>
          <script src={/*React application bundle*/} charSet='UTF-8'/>
        </body>
      </html>
    );
  }
}

(Obviously I excluded a lot of irrelevant server-side rendering logic here).

Finally, we need to configure our networking client. We need to make sure that it sends and accepts cookies to/from all subdomains of our app, and we need to tell it to include the CSRF token header.

With axios(@0.15.2), we need to pass the withCredentials option along with every request:

axios.get(RESOURCE_URL(q), {withCredentials: true})

axios.post(RESOURCES_URL, {body}, {withCredentials: true})

And right at the start of our bundle, we can set a default header like this:

// src/helpers/setGlobalCSRF

module.exports = function(csrfToken) {
  let axiosDefaults = require('axios/lib/defaults');
  axiosDefaults.headers.common['X-CSRF-Token'] = csrfToken;
};


// src/app.jsx

import setGlobalCSRF from 'helpers/setGlobalCSRF';

setGlobalCSRF(window.__csrf_token); // Near top of file somewhere

Now, remember when you're signing in/signing up users you'll need to grab a CSRF token as well:

// src/ducks/Auth/actions.js

export function signupUser(email, password) {
  return function(dispatch) {
    return axios.post(SIGNUP_URL, {email: email.toLowerCase(), password}, {withCredentials: true})
      .then(response => {
        User.loginFromResponse(response);
        //...


// src/models/User/index.js

import setGlobalCSRF from 'helpers/setGlobalCSRF';

export var loginFromResponse = function(response) {
  var {CSRFToken} = response.data;
  setGlobalCSRF(CSRFToken);
};

Nice! Now onto our server

Setting Up The API

Our API server will need the exact same session configuration:

//src/server/index.js

var session = require('express-session'),
    connectRedis = require('connect-redis');

var RedisStore = connectRedis(session);

//...

app.use(session({
  store: new RedisStore({
    url: process.env.REDIS_URL,
    secure: process.env.NODE_ENV=='production'
  }),
  resave: false,
  saveUninitialized: false,
  name: 'connect.sid', // Good idea to make the name somewhat cryptic
  secret: process.env.SESSION_SECRET
}));

//...

And now we're going to have to rewrite any authentication middleware and related routes.

Here's a bit of middleware for checking CSRF tokens:

// controllers/AuthControllers/checkCSRF.js

module.exports = function(req, res, next) {
  var csrfToken = req.get('X-CSRF-Token');

  if (!csrfToken) return res.status(401).json({error: 'CSRF token missing. Please refresh the page.'});

  if (!req.session.csrfToken) {
    return res.status(401).json({error: 'No CSRF token recorded in your session. Please refresh the page.'});
  }

  if (req.session.csrfToken !== csrfToken) return res.status(401).json({error: 'Invalid CSRF token. Please refresh the page.'});

  next();
};

I don't actually add this function as an express-route middleware though, I just call it from requireAuth.js, the actual middleware that my application will use to check authenticate API calls.

// controllers/AuthControllers/requireAuth.js

var User = require('../../models/User'),
    mongoose = require('mongoose'),
    checkCSRF = require('./checkCSRF'),
    ObjectId = mongoose.Types.ObjectId;

module.exports = function(req, res, next) {
  var {user_id} = req.session;

  if (!user_id) return res.status(401).json({error: 'You are not signed in.'});

  try {
    user_id = ObjectId(user_id); // Checking user_id not malformed.
  } catch (err) {
    res.clearCookie('connect.sid'); // Take away their silly cookie.

    return res.status(401).json({error: "Your session is broken! Please sign-in again."});
  }

  // Malformed user_id down here throws error.
  User.findById(user_id, (err, user) => {
    if (err) return next(err);
    if (!user) return res.status(401).json({error: 'Your user does not exist.'});

    req.user = user;

    checkCSRF(req, res, next);
  });
};

Now all that's left to do is make sure our signin/signup routes are doing what they need to: generating and sending a CSRF token, and creating the user's session. 

// controllers/AuthController/signin.js
// controllers/AuthController/signup.js

// After user object is found or created

//...

req.session.user_id = user._id;

var CSRFToken = uuid.v4();
req.session.csrfToken = CSRFToken;

return res.send({
  CSRFToken
});

Logging Out

Logging out is more interesting with sessions. Since we're using htmlOnly cookies, compliant browsers won't allow us to delete them without instruction from the server. To also achieve the side-effect of deleting the user's session data on the server (if desired), I suggest using an API call to logout:

// CLIENT
// src/ducks/Auth/actions.js

return axios.get(LOGOUT_URL, {withCredentials: true})


// SERVER
// controllers/AuthController/logout.js

module.exports = function(req, res, next) {
  // Deletes session data
  req.session.destroy(err => {
    if (err) return next(err);

    res.clearCookie('connect.sid');

    return res.json({});
  });
};

A DELETE request to a Sessions route would probably be more RESTful. My login/signup routes/controllers could be improved similarly.

That's it for this guide. If you find any problems with the code above or need any further guidance, please comment.



-->
ALEXANDER
PATERSON