diff --git a/api-server/src/server/middleware.json b/api-server/src/server/middleware.json index 2788a0de62..69a47f4402 100644 --- a/api-server/src/server/middleware.json +++ b/api-server/src/server/middleware.json @@ -35,6 +35,7 @@ "helmet#noSniff": {}, "helmet#frameguard": {}, "./middlewares/csurf": {}, + "./middlewares/csurf-set-cookie": {}, "./middlewares/constant-headers": {}, "./middlewares/csp": {}, "./middlewares/flash-cheaters": {}, @@ -43,6 +44,7 @@ "files": {}, "final:after": { "./middlewares/sentry-error-handler": {}, + "./middlewares/csurf-error-handler": {}, "./middlewares/error-handlers": {}, "strong-error-handler": { "params": { diff --git a/api-server/src/server/middlewares/csurf-error-handler.js b/api-server/src/server/middlewares/csurf-error-handler.js new file mode 100644 index 0000000000..290dbb01f5 --- /dev/null +++ b/api-server/src/server/middlewares/csurf-error-handler.js @@ -0,0 +1,12 @@ +import { csrfOptions } from './csurf.js'; + +export default function csrfErrorHandler() { + return function (err, req, res, next) { + if (err.code === 'EBADCSRFTOKEN') { + // use the middleware to generate a token. The client sends this back via + // a header + res.cookie('csrf_token', req.csrfToken(), csrfOptions); + } + next(err); + }; +} diff --git a/api-server/src/server/middlewares/csurf-set-cookie.js b/api-server/src/server/middlewares/csurf-set-cookie.js new file mode 100644 index 0000000000..83da1361fd --- /dev/null +++ b/api-server/src/server/middlewares/csurf-set-cookie.js @@ -0,0 +1,13 @@ +import { csrfOptions } from './csurf.js'; + +export default function setCSRFCookie() { + return function (req, res, next) { + // not all paths require a CSRF token, so the function may not be available. + if (req.csrfToken) { + // use the middleware to generate a token. The client sends this back via + // a header + res.cookie('csrf_token', req.csrfToken(), csrfOptions); + } + next(); + }; +} diff --git a/api-server/src/server/middlewares/csurf.js b/api-server/src/server/middlewares/csurf.js index 9c790509c5..126f725d57 100644 --- a/api-server/src/server/middlewares/csurf.js +++ b/api-server/src/server/middlewares/csurf.js @@ -1,12 +1,14 @@ import csurf from 'csurf'; +export const csrfOptions = { + domain: process.env.COOKIE_DOMAIN || 'localhost', + sameSite: 'strict', + secure: process.env.FREECODECAMP_NODE_ENV === 'production' +}; + export default function getCsurf() { const protection = csurf({ - cookie: { - domain: process.env.COOKIE_DOMAIN || 'localhost', - sameSite: 'strict', - secure: process.env.FREECODECAMP_NODE_ENV === 'production' - } + cookie: csrfOptions }); return function csrf(req, res, next) { const { path } = req; @@ -14,8 +16,10 @@ export default function getCsurf() { // eslint-disable-next-line max-len /^\/hooks\/update-paypal$/.test(path) ) { - return next(); + next(); + } else { + // add the middleware + protection(req, res, next); } - return protection(req, res, next); }; } diff --git a/api-server/src/server/utils/getSetAccessToken.js b/api-server/src/server/utils/getSetAccessToken.js index 866a69710b..a7ffe8bfa9 100644 --- a/api-server/src/server/utils/getSetAccessToken.js +++ b/api-server/src/server/utils/getSetAccessToken.js @@ -64,6 +64,7 @@ export function removeCookies(req, res) { res.clearCookie('access_token', config); res.clearCookie('userId', config); res.clearCookie('_csrf', config); + res.clearCookie('csrf_token', config); return; } diff --git a/client/gatsby-browser.js b/client/gatsby-browser.js index 97bb014d75..fe1862a515 100644 --- a/client/gatsby-browser.js +++ b/client/gatsby-browser.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import { I18nextProvider } from 'react-i18next'; +import cookies from 'browser-cookies'; import i18n from './i18n/config'; import { createStore } from './src/redux/createStore'; @@ -27,3 +28,9 @@ wrapRootElement.propTypes = { export const wrapPageElement = layoutSelector; export const disableCorePrefetching = () => true; + +export const onClientEntry = () => { + // purge the _csrf cookie, rather than relying what the browser decides a + // Session duration is + cookies.erase('_csrf'); +}; diff --git a/client/package-lock.json b/client/package-lock.json index bc2c2c733b..ecff36892e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -7131,16 +7131,6 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, - "csrf": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz", - "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==", - "requires": { - "rndm": "1.2.0", - "tsscmp": "1.0.6", - "uid-safe": "2.1.5" - } - }, "css": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", @@ -20956,11 +20946,6 @@ "ret": "~0.1.10" } }, - "random-bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", - "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -22376,11 +22361,6 @@ "inherits": "^2.0.1" } }, - "rndm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz", - "integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w=" - }, "rst-selector-parser": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", @@ -24443,11 +24423,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, - "tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" - }, "tsutils": { "version": "3.19.1", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.19.1.tgz", @@ -24547,14 +24522,6 @@ "typescript-compare": "^0.0.2" } }, - "uid-safe": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", - "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "requires": { - "random-bytes": "~1.0.0" - } - }, "unbox-primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index f44ff1a80f..bd672162d9 100644 --- a/client/package.json +++ b/client/package.json @@ -62,7 +62,6 @@ "buffer": "6.0.3", "chai": "4.3.4", "crypto-browserify": "3.12.0", - "csrf": "3.1.0", "date-fns": "2.21.3", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.6", diff --git a/client/src/utils/ajax.js b/client/src/utils/ajax.js index d9fb422c02..386fa38074 100644 --- a/client/src/utils/ajax.js +++ b/client/src/utils/ajax.js @@ -1,22 +1,20 @@ import envData from '../../../config/env.json'; import axios from 'axios'; -import Tokens from 'csrf'; import cookies from 'browser-cookies'; const { apiLocation } = envData; const base = apiLocation; -const tokens = new Tokens(); axios.defaults.withCredentials = true; -// _csrf is passed to the client as a cookie. Tokens are sent back to the server -// via headers: +// csrf_token is passed to the client as a cookie. The client must send +// this back as a header. function setCSRFTokens() { - const _csrf = typeof window !== 'undefined' && cookies.get('_csrf'); - if (!_csrf) return; - axios.defaults.headers.post['CSRF-Token'] = tokens.create(_csrf); - axios.defaults.headers.put['CSRF-Token'] = tokens.create(_csrf); + const csrfToken = typeof window !== 'undefined' && cookies.get('csrf_token'); + if (!csrfToken) return; + axios.defaults.headers.post['CSRF-Token'] = csrfToken; + axios.defaults.headers.put['CSRF-Token'] = csrfToken; } function get(path) {