Как реализовать защиту CSRF в Nextjs с Apollo и GraphQL

Следуя этот пример в репозитории Nextjs, я хочу реализовать защиту CSRF (возможно, с пакетом csurf), потому что я использую cookie идентификатора сеанса с экспресс-сеансом.

Я попытался установить csurf на своем настраиваемом сервере и сохранить сгенерированный токен в res.locals.csrfToken, который можно использовать при загрузке первой страницы статическим методом getInitialProps, который находится в /lib/withApollo.js в примере I связаны. Как только я пытаюсь сменить страницу (со ссылками) или попытаться сделать почтовый запрос с помощью apollo (например, войти в систему), сервер меняет токен csrf, поэтому тот, который использовался Apollo, больше не полезен, и поэтому я получаю ошибка «csrf is invalid».

Собственный сервер с настройкой csurf

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
    }
  );

С этой конфигурацией каждый маршрут защищен от csrf, но токен, созданный на сервере, часто меняется, и Apollo не может получить обновленный, как только ему нужно, поэтому первая загрузка успешна, но последующее изменение страницы (ссылки) или любой почтовый запрос не выполняется из-за изменения токена.

Вы получили решение? У меня такая же проблема

Shifut Hossain 12.06.2019 21:23

Я тоже здесь застрял. Любые идеи??

Ngatia Frankline 11.07.2019 22:46
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Что такое Apollo Client и зачем он нужен?
Что такое Apollo Client и зачем он нужен?
Apollo Client - это полнофункциональный клиент GraphQL для JavaScript-приложений, который упрощает получение, управление и обновление данных в...
5
2
10 189
3

Ответы 3

Возможно, это не тот ответ, который вы ищете. Я прочитал здесь, что если вы используете JWT, нет необходимости в CSRFToken. Не совсем уверен, но пока это единственное, что нужно сделать.

Бенджамин М объясняет следующее:

Я нашел информацию о CSRF + без использования файлов cookie для аутентификации:

https://auth0.com/blog/2014/01/07/angularjs-authentication-with-cookies-vs-token/ "поскольку вы не полагаетесь на файлы cookie, вам не нужно защищаться от межсайтовых запросов"

http://angular-tips.com/blog/2014/05/json-web-tokens-introduction/ «Если мы пойдем по пути файлов cookie, вам действительно понадобится CSRF, чтобы избежать межсайтовых запросов. Это то, о чем мы можем забыть при использовании JWT, как вы увидите». (JWT = Json Web Token, аутентификация на основе токенов для приложений без сохранения состояния)

http://www.jamesward.com/2013/05/13/securing-single-page-apps-and-rest-services «Самый простой способ выполнить аутентификацию, не рискуя уязвимостью CSRF, - просто избегать использования файлов cookie для идентификации пользователя»

http://sitr.us/2011/08/26/cookies-are-bad-for-you.html «Самая большая проблема с CSRF заключается в том, что файлы cookie не обеспечивают абсолютно никакой защиты от атак этого типа. Если вы используете аутентификацию файлов cookie, вы также должны принять дополнительные меры для защиты от CSRF. приложение никогда не выполняет никаких побочных эффектов в ответ на запросы GET ".

Есть еще много страниц, на которых указано, что вам не нужна защита CSRF, если вы не используете файлы cookie для аутентификации. Конечно, вы все еще можете использовать файлы cookie для всего остального, но не храните в них что-либо вроде session_id.

Полная статья здесь: Требуется ли токен CSRF при использовании аутентификации без сохранения состояния (= без сеанса)?

Обновлять

После стольких просмотров я наконец смог отправить csrf cookie. Я думаю, что проблема связана со словом return. Когда вы используете return, он исключает cookie. Это то, что я сделал, отредактировав /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
        }
      };
    });
  });

пре !! Однако SSR не имеет файлов cookie. Я думаю, у нас должно быть две конечные точки от клиента и еще одна для SSR. URL-адрес SSR может быть исключен из csrf.

Для тех, кто не использует экспресс-сессию, у меня также работает приведенный ниже код. Надеюсь, это поможет другим, кому это может понадобиться. Я использую собственный сервер Express, и вот упрощенная версия моей реализации.

Server.js (настраиваемый экспресс-сервер)

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.info('listening on port ' + PORT);
  });
});

Затем мы можем получить клиентскую часть XSRF-TOKEN из document.cookie в _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,
};

Единственное, в чем я не уверен, это то, имеет ли передача токена во внешний интерфейс какие-либо последствия для безопасности? У меня всегда было впечатление, что CSRF обрабатывается только на стороне сервера. Однако в документации по csurf есть примеры для React, где они передают его либо в тело запроса, либо в заголовок. Может быть, кто-нибудь, кто разбирается в вопросах безопасности, поделится своим опытом?

Поскольку мы не используем сеансы, сервер генерирует два токена, один из которых называется _csrf - это нормально, поскольку это секрет, по которому csurf будет проверять.

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.

Другие вопросы по теме