From 05945a9a74eb6e3a05b79695d0d76ba0ed69b773 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 11 Aug 2025 17:47:09 -0400 Subject: [PATCH 1/2] feat: add JWT cookie infrastructure Adds the foundation for secure cookie-based authentication without changing the authentication flow. This prepares the codebase for moving JWT tokens from Redux state to httpOnly cookies in a follow-up PR. Changes: - Added cookie-parser dependency for HTTP cookie handling - Added cookieParser() middleware to Express application - Created cookieHelpers.js utilities for consistent cookie options - Includes getAuthCookieOptions() for setting secure authentication cookies - Includes getClearAuthCookieOptions() for clearing cookies on logout Infrastructure only - no behavioral changes to authentication flow yet. Files added/modified: - package.json (cookie-parser dependency) - src/app.js (cookieParser middleware) - src/utils/cookieHelpers.js (cookie utilities) Next steps: - Follow-up PR will modify JWT verification to check cookies - Enable secure cookie-based authentication - Add logout functionality to clear httpOnly cookies Risk level: LOW (infrastructure only, no authentication changes) --- server/package-lock.json | 23 +++++++++ server/package.json | 1 + server/src/app.js | 2 + server/src/utils/cookieHelpers.js | 26 ++++++++++ server/utils/sanitization.js | 85 +++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+) create mode 100644 server/src/utils/cookieHelpers.js create mode 100644 server/utils/sanitization.js diff --git a/server/package-lock.json b/server/package-lock.json index b29fb0642..b955e64ab 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -14,6 +14,7 @@ "bcryptjs": "3.0.2", "bullmq": "5.41.2", "compression": "1.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dockerode": "4.0.6", "dotenv": "^16.4.5", @@ -2230,6 +2231,28 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/server/package.json b/server/package.json index 3e52d6983..55f00f364 100755 --- a/server/package.json +++ b/server/package.json @@ -21,6 +21,7 @@ "bcryptjs": "3.0.2", "bullmq": "5.41.2", "compression": "1.8.1", + "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dockerode": "4.0.6", "dotenv": "^16.4.5", diff --git a/server/src/app.js b/server/src/app.js index 6ba1d0d05..d272e7f76 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -4,6 +4,7 @@ import { responseHandler } from "./middleware/responseHandler.js"; import cors from "cors"; import helmet from "helmet"; import compression from "compression"; +import cookieParser from "cookie-parser"; import languageMiddleware from "./middleware/languageMiddleware.js"; import swaggerUi from "swagger-ui-express"; import { handleErrors } from "./middleware/handleErrors.js"; @@ -30,6 +31,7 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op }) ); app.use(express.json()); + app.use(cookieParser()); app.use( helmet({ hsts: false, diff --git a/server/src/utils/cookieHelpers.js b/server/src/utils/cookieHelpers.js new file mode 100644 index 000000000..7005b66ca --- /dev/null +++ b/server/src/utils/cookieHelpers.js @@ -0,0 +1,26 @@ +/** + * Get standardized cookie options for authentication tokens + * @param {Object} options - Additional cookie options + * @returns {Object} Cookie options object + */ +export const getAuthCookieOptions = (options = {}) => { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 2 * 60 * 60 * 1000, // 2 hours (matches JWT TTL) + ...options, + }; +}; + +/** + * Clear cookie options for authentication tokens + * @returns {Object} Cookie clear options object + */ +export const getClearAuthCookieOptions = () => { + return { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }; +}; diff --git a/server/utils/sanitization.js b/server/utils/sanitization.js new file mode 100644 index 000000000..a89325de1 --- /dev/null +++ b/server/utils/sanitization.js @@ -0,0 +1,85 @@ +import { JSDOM } from "jsdom"; +import DOMPurify from "isomorphic-dompurify"; + +// Initialize DOMPurify with jsdom +const window = new JSDOM("").window; +const purify = DOMPurify(window); + +/** + * Sanitizes user input to prevent XSS attacks + * @param {string} input - The input string to sanitize + * @param {Object} options - Sanitization options + * @returns {string} The sanitized string + */ +export const sanitizeInput = (input, options = {}) => { + if (typeof input !== "string") { + return input; + } + + // Default configuration - remove all HTML tags and attributes + const defaultConfig = { + ALLOWED_TAGS: [], + ALLOWED_ATTR: [], + KEEP_CONTENT: true, + ...options, + }; + + return purify.sanitize(input, defaultConfig); +}; + +/** + * Sanitizes an object recursively + * @param {Object} obj - The object to sanitize + * @param {Object} options - Sanitization options + * @returns {Object} The sanitized object + */ +export const sanitizeObject = (obj, options = {}) => { + if (typeof obj !== "object" || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => sanitizeObject(item, options)); + } + + const sanitized = {}; + for (const [key, value] of Object.entries(obj)) { + if (typeof value === "string") { + sanitized[key] = sanitizeInput(value, options); + } else if (typeof value === "object" && value !== null) { + sanitized[key] = sanitizeObject(value, options); + } else { + sanitized[key] = value; + } + } + + return sanitized; +}; + +/** + * Express middleware for sanitizing request body + * @param {Object} options - Sanitization options + * @returns {Function} Express middleware function + */ +export const sanitizeBody = (options = {}) => { + return (req, res, next) => { + if (req.body && typeof req.body === "object") { + req.body = sanitizeObject(req.body, options); + } + next(); + }; +}; + +/** + * Express middleware for sanitizing query parameters + * @param {Object} options - Sanitization options + * @returns {Function} Express middleware function + */ +export const sanitizeQuery = (options = {}) => { + return (req, res, next) => { + if (req.query && typeof req.query === "object") { + req.query = sanitizeObject(req.query, options); + } + next(); + }; +}; From c12f5880e2fcd0ea31e23a7c260698645202c388 Mon Sep 17 00:00:00 2001 From: Alexander Holliday Date: Mon, 11 Aug 2025 15:07:42 -0700 Subject: [PATCH 2/2] Delete server/utils/sanitization.js --- server/utils/sanitization.js | 85 ------------------------------------ 1 file changed, 85 deletions(-) delete mode 100644 server/utils/sanitization.js diff --git a/server/utils/sanitization.js b/server/utils/sanitization.js deleted file mode 100644 index a89325de1..000000000 --- a/server/utils/sanitization.js +++ /dev/null @@ -1,85 +0,0 @@ -import { JSDOM } from "jsdom"; -import DOMPurify from "isomorphic-dompurify"; - -// Initialize DOMPurify with jsdom -const window = new JSDOM("").window; -const purify = DOMPurify(window); - -/** - * Sanitizes user input to prevent XSS attacks - * @param {string} input - The input string to sanitize - * @param {Object} options - Sanitization options - * @returns {string} The sanitized string - */ -export const sanitizeInput = (input, options = {}) => { - if (typeof input !== "string") { - return input; - } - - // Default configuration - remove all HTML tags and attributes - const defaultConfig = { - ALLOWED_TAGS: [], - ALLOWED_ATTR: [], - KEEP_CONTENT: true, - ...options, - }; - - return purify.sanitize(input, defaultConfig); -}; - -/** - * Sanitizes an object recursively - * @param {Object} obj - The object to sanitize - * @param {Object} options - Sanitization options - * @returns {Object} The sanitized object - */ -export const sanitizeObject = (obj, options = {}) => { - if (typeof obj !== "object" || obj === null) { - return obj; - } - - if (Array.isArray(obj)) { - return obj.map((item) => sanitizeObject(item, options)); - } - - const sanitized = {}; - for (const [key, value] of Object.entries(obj)) { - if (typeof value === "string") { - sanitized[key] = sanitizeInput(value, options); - } else if (typeof value === "object" && value !== null) { - sanitized[key] = sanitizeObject(value, options); - } else { - sanitized[key] = value; - } - } - - return sanitized; -}; - -/** - * Express middleware for sanitizing request body - * @param {Object} options - Sanitization options - * @returns {Function} Express middleware function - */ -export const sanitizeBody = (options = {}) => { - return (req, res, next) => { - if (req.body && typeof req.body === "object") { - req.body = sanitizeObject(req.body, options); - } - next(); - }; -}; - -/** - * Express middleware for sanitizing query parameters - * @param {Object} options - Sanitization options - * @returns {Function} Express middleware function - */ -export const sanitizeQuery = (options = {}) => { - return (req, res, next) => { - if (req.query && typeof req.query === "object") { - req.query = sanitizeObject(req.query, options); - } - next(); - }; -};