From cb20ed4f770c761a111d4448537573c304601313 Mon Sep 17 00:00:00 2001 From: "Markus J. Wetzel" Date: Sat, 26 Feb 2022 12:39:26 +0100 Subject: [PATCH] Update cookie handling --- src/bootstrap/client/start.js | 18 +---- src/bootstrap/server/createHttpServer.js | 2 +- .../server/createReactAppOnServer.js | 57 ++++++++------- src/bootstrap/server/defaultConfig.js | 14 ++-- .../{ => server}/utils/createProxy.js | 0 .../{ => server}/utils/detectDevice.js | 10 +-- .../{ => server}/utils/detectLocale.js | 0 .../utils/generateHtmlSnippets.js | 0 src/bootstrap/server/utils/getRealPath.js | 12 ++++ .../server/utils/handleCsrfProtection.js | 33 +++++++++ src/bootstrap/utils/CookieJar.js | 69 ------------------- src/bootstrap/utils/getRealPath.js | 12 ---- src/bootstrap/utils/handleCsrfProtection.js | 25 ------- 13 files changed, 88 insertions(+), 164 deletions(-) rename src/bootstrap/{ => server}/utils/createProxy.js (100%) rename src/bootstrap/{ => server}/utils/detectDevice.js (66%) rename src/bootstrap/{ => server}/utils/detectLocale.js (100%) rename src/bootstrap/{ => server}/utils/generateHtmlSnippets.js (100%) create mode 100644 src/bootstrap/server/utils/getRealPath.js create mode 100644 src/bootstrap/server/utils/handleCsrfProtection.js delete mode 100644 src/bootstrap/utils/CookieJar.js delete mode 100644 src/bootstrap/utils/getRealPath.js delete mode 100644 src/bootstrap/utils/handleCsrfProtection.js diff --git a/src/bootstrap/client/start.js b/src/bootstrap/client/start.js index 4e2220b..3dd40d4 100644 --- a/src/bootstrap/client/start.js +++ b/src/bootstrap/client/start.js @@ -4,7 +4,6 @@ import 'react-app-polyfill/stable'; // import react-dom import ReactDOM from 'react-dom'; -import CookieJar from '../utils/CookieJar'; /* eslint-disable no-underscore-dangle */ const data = window.__DATA__; @@ -30,22 +29,7 @@ const render = (component) => { // eslint-disable-next-line import/no-unresolved const hydrate = require('appClientEntry').default; -const cookies = new CookieJar(); - -const headers = { - create: () => ctx.network.csrfHeader, -}; - -const ctxClientOnly = { - ...ctx, - network: { - ...ctx.network, - cookies, - headers, - }, -}; - -hydrate(ctxClientOnly, { render, root }, data); +hydrate(ctx, { render, root }, data); if (process.env.APP_MODE === 'development' && !ssr) { // eslint-disable-next-line no-console diff --git a/src/bootstrap/server/createHttpServer.js b/src/bootstrap/server/createHttpServer.js index 91a6e4e..7929703 100644 --- a/src/bootstrap/server/createHttpServer.js +++ b/src/bootstrap/server/createHttpServer.js @@ -1,7 +1,7 @@ const Express = require('express'); const http = require('http'); const compression = require('compression'); -const createProxy = require('../utils/createProxy'); +const createProxy = require('./utils/createProxy'); const createReactAppOnServer = require('./createReactAppOnServer'); const paths = require('../../config/paths'); diff --git a/src/bootstrap/server/createReactAppOnServer.js b/src/bootstrap/server/createReactAppOnServer.js index 6354083..796ea4d 100644 --- a/src/bootstrap/server/createReactAppOnServer.js +++ b/src/bootstrap/server/createReactAppOnServer.js @@ -1,32 +1,43 @@ const ReactDOMServer = require('react-dom/server'); const path = require('path'); -const CookieJar = require('../utils/CookieJar'); -const detectDevice = require('../utils/detectDevice'); -const detectLocale = require('../utils/detectLocale'); -const handleCsrfProtection = require('../utils/handleCsrfProtection'); -const getRealPath = require('../utils/getRealPath'); -const generateHtmlSnippets = require('../utils/generateHtmlSnippets'); +const cookie = require('cookie'); +const detectDevice = require('./utils/detectDevice'); +const detectLocale = require('./utils/detectLocale'); +const handleCsrfProtection = require('./utils/handleCsrfProtection'); +const getRealPath = require('./utils/getRealPath'); +const generateHtmlSnippets = require('./utils/generateHtmlSnippets'); const paths = require('../../config/paths'); +const getCookies = (req, res) => { + const cache = cookie.parse(req.headers.cookie); + + return { + get(name) { + return cache[name]; + }, + set(name, value, options) { + res.cookie(name, value, options); + }, + }; +}; + module.exports = function createAppOnServer(config) { return (req, res) => { - const cookies = new CookieJar(req, res); + const cookies = getCookies(req, res); + const userAgent = req.headers['user-agent']; + const { url } = req; const [locale, localeSource] = detectLocale(req, cookies, config.intl); - const device = detectDevice(req, cookies, config.media); - const csrfHeader = handleCsrfProtection(cookies, config.network); - const [basename, realPath] = getRealPath(req, locale, localeSource); + const device = detectDevice(userAgent, cookies, config.device); + const csrf = handleCsrfProtection(cookies, config.csrf); + const [basename, realPath] = getRealPath(url, locale, localeSource); const ctx = { basename, path: realPath, ssr: config.ssr, - network: { - csrfHeader, - }, - media: { - device, - }, + csrf, + device, intl: { locale, localeSource, @@ -35,22 +46,10 @@ module.exports = function createAppOnServer(config) { }, }; - const headers = { - create: () => ({ - cookie: cookies.serialize(), - ...csrfHeader, - }), - }; - const ctxServerOnly = { - ...ctx, req, res, - network: { - ...ctx.network, - cookies, - headers, - }, + ...ctx, }; // define render, redirect and error function for hydrate function diff --git a/src/bootstrap/server/defaultConfig.js b/src/bootstrap/server/defaultConfig.js index e35f352..dee32c2 100644 --- a/src/bootstrap/server/defaultConfig.js +++ b/src/bootstrap/server/defaultConfig.js @@ -3,14 +3,14 @@ module.exports = { port: 8080, proxies: [], ssr: true, - network: { - csrfProtection: true, - csrfHeaderName: 'X-Csrf-Token', - csrfCookieName: 'csrf', + csrf: { + protection: true, + headerName: 'X-Csrf-Token', + cookieName: 'csrf', }, - media: { - deviceDetection: true, - deviceCookieName: 'view', + device: { + detection: true, + cookieName: 'view', }, intl: { localeDetection: true, diff --git a/src/bootstrap/utils/createProxy.js b/src/bootstrap/server/utils/createProxy.js similarity index 100% rename from src/bootstrap/utils/createProxy.js rename to src/bootstrap/server/utils/createProxy.js diff --git a/src/bootstrap/utils/detectDevice.js b/src/bootstrap/server/utils/detectDevice.js similarity index 66% rename from src/bootstrap/utils/detectDevice.js rename to src/bootstrap/server/utils/detectDevice.js index 7e988bd..494c641 100644 --- a/src/bootstrap/utils/detectDevice.js +++ b/src/bootstrap/server/utils/detectDevice.js @@ -1,12 +1,14 @@ const DeviceDetector = require('device-detector-js'); -module.exports = function detectDevice(req, cookies, config) { +module.exports = function detectDevice(source, cookies, config) { + const { detection, cookieName } = config; + // we don't want to detect the device - if (!config.deviceDetection) { + if (!detection) { return null; } - const cookie = cookies.get(config.deviceCookieName); + const cookie = cookies.get(cookieName); // device is set by cookie if (cookie === 'mobile' || cookie === 'desktop') { @@ -15,7 +17,7 @@ module.exports = function detectDevice(req, cookies, config) { // detect device from user agent const deviceDetector = new DeviceDetector(); - const device = deviceDetector.parse(req.headers['user-agent']); + const device = deviceDetector.parse(source); if (device.device && device.device.type === 'smartphone') { // phone diff --git a/src/bootstrap/utils/detectLocale.js b/src/bootstrap/server/utils/detectLocale.js similarity index 100% rename from src/bootstrap/utils/detectLocale.js rename to src/bootstrap/server/utils/detectLocale.js diff --git a/src/bootstrap/utils/generateHtmlSnippets.js b/src/bootstrap/server/utils/generateHtmlSnippets.js similarity index 100% rename from src/bootstrap/utils/generateHtmlSnippets.js rename to src/bootstrap/server/utils/generateHtmlSnippets.js diff --git a/src/bootstrap/server/utils/getRealPath.js b/src/bootstrap/server/utils/getRealPath.js new file mode 100644 index 0000000..3981da3 --- /dev/null +++ b/src/bootstrap/server/utils/getRealPath.js @@ -0,0 +1,12 @@ +module.exports = function getRealPath(url, locale, localeSource) { + if (localeSource !== 'url') { + const basename = ''; + + return [basename, url]; + } + + const basename = `/${locale}`; + const path = url.substring(locale.length + 1, url.length); + + return [basename, path]; +}; diff --git a/src/bootstrap/server/utils/handleCsrfProtection.js b/src/bootstrap/server/utils/handleCsrfProtection.js new file mode 100644 index 0000000..c647d6c --- /dev/null +++ b/src/bootstrap/server/utils/handleCsrfProtection.js @@ -0,0 +1,33 @@ +const randomString = require('randomstring'); + +const TOKEN_LENGTH = 32; + +module.exports = function handleCsrfProtection(cookies, config) { + const { protection, cookieName, headerName } = config; + + if (!protection) { + return { + headers: {}, + }; + } + + const cookie = cookies.get(cookieName); + + if (cookie && cookie.length === TOKEN_LENGTH) { + return { + headers: { + [headerName]: cookie, + }, + }; + } + + const token = randomString.generate(TOKEN_LENGTH); + + cookies.set(cookieName, token, { httpOnly: true }); + + return { + headers: { + [headerName]: token, + }, + }; +}; diff --git a/src/bootstrap/utils/CookieJar.js b/src/bootstrap/utils/CookieJar.js deleted file mode 100644 index b44caf6..0000000 --- a/src/bootstrap/utils/CookieJar.js +++ /dev/null @@ -1,69 +0,0 @@ -const cookie = require('cookie'); - -const getFromDocument = () => (document && document.cookie) || ''; - -class CookieJar { - constructor(req, res) { - this.server = !!(req && res); - - this.cookies = cookie.parse(this.server ? req.headers.cookie : getFromDocument()); - - this.res = res; - } - - get(name) { - // refresh cookies before get - if (!this.server) { - this.cookies = cookie.parse(getFromDocument()); - } - - return this.cookies[name]; - } - - set(name, value, options) { - this.cookies[name] = value; - - if (this.server) { - this.res.cookie(name, value, options); - } else { - document.cookie = cookie.serialize(name, value, options); - } - } - - setFromResponse(headers) { - if (!headers['set-cookie']) { - return; - } - - headers['set-cookie'].forEach((raw) => { - const [pair, ...rawOptions] = raw.split(';'); - - const pairs = cookie.parse(pair); - const keys = Object.keys(pairs); - - if (keys.length !== 1) { - return; - } - - const options = cookie.parse(rawOptions.join(';')); - - this.set(keys[0], pairs[keys[0]], { - expires: options.Expires, - maxAge: options['Max-Age'], - domain: options.Domain, - path: options.Path, - secure: options.Secure, - httpOnly: options.HttpOnly, - sameSite: options.SameSite, - }); - }); - } - - serialize() { - return Object.keys(this.cookies) - .map((key) => `${key}=${this.cookies[key]}`) - .join(';'); - } -} - -module.exports = CookieJar; diff --git a/src/bootstrap/utils/getRealPath.js b/src/bootstrap/utils/getRealPath.js deleted file mode 100644 index c65e13e..0000000 --- a/src/bootstrap/utils/getRealPath.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = function getRealPath(req, locale, localeSource) { - if (localeSource !== 'url') { - const basename = ''; - - return [basename, req.url]; - } - - const basename = `/${locale}`; - const path = req.url.substring(locale.length + 1, req.url.length); - - return [basename, path]; -}; diff --git a/src/bootstrap/utils/handleCsrfProtection.js b/src/bootstrap/utils/handleCsrfProtection.js deleted file mode 100644 index ae7896e..0000000 --- a/src/bootstrap/utils/handleCsrfProtection.js +++ /dev/null @@ -1,25 +0,0 @@ -const randomString = require('randomstring'); - -const TOKEN_LENGTH = 32; - -module.exports = function handleCsrfProtection(cookies, config) { - if (!config.csrfProtection) { - return {}; - } - - const cookie = cookies.get(config.csrfCookieName); - - if (cookie && cookie.length === TOKEN_LENGTH) { - return { - [config.csrfHeaderName]: cookie, - }; - } - - const token = randomString.generate(TOKEN_LENGTH); - - cookies.set(config.csrfCookieName, token, { httpOnly: true }); - - return { - [config.csrfHeaderName]: token, - }; -};