How to implement CSRF protection in Nextjs with Apollo and GraphQL
Asked Answered
I

3

5

Following this example in Nextjs repository, I want to implement the CSRF protection (perhaps with csurf package), because I'm using a session ID cookie with express-session.

I tried setting csurf in my custom server and save the generated token in res.locals.csrfToken which can be taken, on first page load, by the static method "getInitialProps" which is located in /lib/withApollo.js in the example I linked. As soon as I try to change page (with links) or try to make a post request with apollo (login, for instance), server changes the csrf token, so the one which was used by Apollo is no more useful and so I get a "csrf is invalid" error.

Custom server with csurf configuration

const csrf = require('csurf');
const csrfProtection = csrf();
////express-session configuration code////
app.use(csrfProtection);
app.use((req, res, next) => {
    res.locals.csrfToken = req.csrfToken();
next();
})

/lib/initApollo.js

function create(initialState, { getToken, cookies, csrfToken }) {
  const httpLink = createHttpLink({
    uri: "http://localhost:3000/graphql",
    credentials: "include"
  });

    const authLink = setContext((_, { headers }) => {
    const token = getToken();
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : "",
        Cookie: cookies ? cookies : "",
        "x-xsrf-token": csrfToken ? csrfToken : ""
      }
    };
  });

/lib/withApollo.js

static async getInitialProps(ctx) {
  const {
    Component,
    router,
    ctx: { req, res }
  } = ctx;
  const apollo = initApollo(
    {},
    {
      getToken: () => parseCookies(req).token,
      cookies: req ? req.headers.cookie : "",
      csrfToken: res ? res.locals.csrfToken : document.cookie
    }
  );

With this config, every route is protected against csrf, but the token created on the server often change and Apollo can't retrieve the updated one as soon as it needs, so the first load is successful, but the subsequent page change (links) or any post request fails, because the token has changed.

Isoleucine answered 13/1, 2019 at 11:4 Comment(2)
Did you get the solution? I'm facing the same problemTryma
Stuck here too. Any ideas??Observer
O
5

Update

After so much browsing i finally was able to send csrf cookie. I think the problem lies with with the word return.When you use return it excludes the cookie. This is what i did by editing /lib/initApollo.js.


function create(initialState, { getToken, cookies, csrfToken }) {
  const httpLink = createHttpLink({
    uri: "http://localhost:3000/graphql",
    credentials: "include"
  });

    const authLink = setContext((_, { headers }) => {
      const token = getToken();
      return {
        headers: {
          ...headers,
          authorization: token ? `Bearer ${token}` : "",
          "x-xsrf-token": csrfToken ? csrfToken : ""
        }
        cookies: {
          ...cookies
        }
      };
    });
  });

pree!! However SSR does not have cookies. I think we should have two endpoint from client and another for SSR. The SSR url can be csrf exempted.

Observer answered 27/7, 2019 at 22:58 Comment(0)
O
2

This may not be the answer you are looking for. I have read it here that if you are using JWT there is no need for CSRFToken. Am not completely sure but its the only get going for now.

Benjamin M explains as follow:

I found some information about CSRF + using no cookies for authentication:

https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/ "since you are not relying on cookies, you don't need to protect against cross site requests"

http://angular-tips.com/blog/2014/05/json-web-tokens-introduction/ "If we go down the cookies way, you really need to do CSRF to avoid cross site requests. That is something we can forget when using JWT as you will see." (JWT = Json Web Token, a Token based authentication for stateless apps)

http://www.jamesward.com/2013/05/13/securing-single-page-apps-and-rest-services "The easiest way to do authentication without risking CSRF vulnerabilities is to simply avoid using cookies to identify the user"

http://sitr.us/2011/08/26/cookies-are-bad-for-you.html "The biggest problem with CSRF is that cookies provide absolutely no defense against this type of attack. If you are using cookie authentication you must also employ additional measures to protect against CSRF. The most basic precaution that you can take is to make sure that your application never performs any side-effects in response to GET requests."

There are plenty more pages, which state that you don't need any CSRF protection, if you don't use cookies for authentication. Of course you can still use cookies for everything else, but avoid storing anything like session_id inside it.

Full article here: CSRF Token necessary when using Stateless(= Sessionless) Authentication?

Observer answered 11/7, 2019 at 21:12 Comment(0)
P
0

For those who do not use express-session, the below code also works for me. I hope this helps others who might need it. I'm using a custom Express server and here's a simplified version of my implementation.

Server.js (custom express server)

const express = require('express');
const next = require('next');
const url = require('url');
var csrf = require('csurf');
const cookieParser = require('cookie-parser');

// NextJS Configuration
const dev = process.env.NODE_ENV !== 'production';
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();

// Initiate the Express app
const PORT = process.env.PORT || 5000;
const app = express();

// CSRF protection middleware
var csrfProtection = csrf({ cookie: true });

// Initiate the NextApp
nextApp.prepare().then(() => {
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use(cookieParser(process.env.COOKIE_PARSER_SECRET));

  // If you do not want your API routes protected with CSRF tokens, do not include the middlware
  app.use('/api/v1/wide-open', (req, res, next) => {
    return res.status(200).json({ message: 'This route is wide open' });
  });

  // If you want your API routes protected with CSRF
  app.use('/api/v1/protect-me', csrfProtection, (req, res, next) => {
    res.status(200).json({
      message: 'I am very safe',
    });
  });

  // Initialize CSRF to send a token to the front-end
  app.use(csrf({ cookie: true }));

  //catch-all for nextJS /pages
  app.get('*', (req, res) => {
    res.set({
      'Cache-Control': 'public, max-age=3600',
    });

    // It is important that the below two lines are inserted within the app.get('*') route
    const token = req.csrfToken();
    res.cookie('XSRF-TOKEN', token);

    const parsedUrl = url.parse(req.url, true);
    return handle(req, res, parsedUrl);
  });

  app.listen(PORT, (err) => {
    if (err) throw err;
    console.log('listening on port ' + PORT);
  });
});

We can then get the XSRF-TOKEN client side from document.cookie in _app.js

_app.js

import React, { useEffect } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import Head from 'next/head';

export default function MyApp(props) {
  const { Component, pageProps } = props;

  useEffect(() => {
    // Get the XSRF-TOKEN from cookies
    function getCookie(name) {
      const value = `; ${document.cookie}`;
      const parts = value.split(`; ${name}=`);
      if (parts.length === 2) return parts.pop().split(';').shift();
    }

    // set the 'csrf-token' as header on Axios POST requests only (please see csurf docs to see which other headers they accept)
    // you could also add PUT or PATCH if you wish
    axios.defaults.headers.post['csrf-token'] = getCookie('XSRF-TOKEN');

    // The rest of your UseEffect code (if any).....
  }, []);

  // Your app
  return (
    <React.Fragment>
      <Head></Head>
      <Navbar />
      <Component {...pageProps} />
      <Footer />
    </React.Fragment>
  );
}

MyApp.propTypes = {
  Component: PropTypes.elementType.isRequired,
  pageProps: PropTypes.object.isRequired,
};

The only thing I am unsure of is whether passing the token to the front-end has any security implications? I was always under the impression CSRF is handled server-side only. However in the documentation for csurf they have examples for React where they pass it to either a req body or header. Maybe someone with more security insights can share their expertise?

Because we are not using sessions, the server generates two tokens, one called _csrf - this is normal, as this is the secret that csurf will validate against.

Notes If you implement it this way and you are testing in Postman / Insomnia, a regular POST request will be rejected by the csurf middleware. So you'll first have to do a GET request to your website (or http://localhost:PORT in dev) and get the csrf token from the cookie it returns. This gets a bit annoying, so you can remove the middleware while you're in dev mode and make sure to add it back before you move to production.

Perez answered 6/4, 2021 at 9:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.