From 0ae9a5177b1a8c3b89bf9c1a80e4b58fbbce215f Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 20 Apr 2026 15:47:42 +0200 Subject: [PATCH 1/3] Add log throttle for JWT messages for 5 minutes by composite key --- Framework/Backend/http/server.js | 47 +++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/Framework/Backend/http/server.js b/Framework/Backend/http/server.js index 16a1c97c1..368b5eefa 100644 --- a/Framework/Backend/http/server.js +++ b/Framework/Backend/http/server.js @@ -78,6 +78,13 @@ class HttpServer { this.listen(); } + /** + * Map to keep track of similar logs by key + * @key {string} - combination of IP address, error name and message + * @value {object.count} - count of occurrences of the same error by the same key + * @value {object.lastLoggedTimestamp} - timestamp of the last log of the same error by the same key + */ + this._jwtErrorsByIp = new Map(); this.logger = LogManager.getLogger(`${process.env.npm_config_log_label ?? 'framework'}/server`); } @@ -503,6 +510,43 @@ class HttpServer { return this.server; } + /** + * Logs a errors with throttling by IP address every 5 minutes with the first error logged immediately + * and rest of occurrences after 5 minutes with number of occurrences. + * @param {string} ip - IP address of the request + * @param {string} name - Error name + * @param {string} message - Error message + * @private + */ + _logErrorWithThrottling(ip, name, message) { + const now = Date.now(); + const compositeKey = `${ip}/${name}/${message}`; + + if (!this._jwtErrorsByIp.has(compositeKey)) { + this.logger.errorMessage(`${name} : ${message} (IP: ${ip})`); + this._jwtErrorsByIp.set(compositeKey, { + count: 1, + lastLoggedTimestamp: now, + }); + } else { + const fiveMinutes = 5 * 60 * 1000; + const lastErrorData = this._jwtErrorsByIp.get(compositeKey); + const timeSinceLastLog = now - lastErrorData.lastLoggedTimestamp; + + if (timeSinceLastLog >= fiveMinutes) { + if (lastErrorData.count > 1) { + this.logger.errorMessage(`${name} : ${message} (IP: ${ip}) (occurrences: ${lastErrorData.count})`); + } else { + this.logger.errorMessage(`${name} : ${message} (IP: ${ip})`); + } + lastErrorData.count = 1; + lastErrorData.lastLoggedTimestamp = now; + } else { + lastErrorData.count++; + } + } + } + /** * Verifies JWT token synchronously. * @todo use promises or generators to call it asynchronously! @@ -514,7 +558,7 @@ class HttpServer { try { this.jwtAuthenticate(req); } catch ({ name, message }) { - this.logger.errorMessage(`${name} : ${message}`); + this._logErrorWithThrottling(req.ip, name, message); res.status(403).json({ error: '403 - Json Web Token Error', @@ -531,6 +575,7 @@ class HttpServer { * * @param {Request} req - Express Request object * @return {void} resolves once the request is filled with authentication, and reject if jwt verification failed + * @throws {UnauthorizedAccessError} if token is invalid, expired or secret is wrong */ jwtAuthenticate(req) { const data = this.o2TokenService.verify(req.query.token); From 099f8ff3cb88de52efa05e66b920272ea22375d7 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 20 Apr 2026 15:48:33 +0200 Subject: [PATCH 2/3] If in production, allow to trust proxy server --- Framework/Backend/http/server.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Framework/Backend/http/server.js b/Framework/Backend/http/server.js index 368b5eefa..7c975e8e1 100644 --- a/Framework/Backend/http/server.js +++ b/Framework/Backend/http/server.js @@ -46,6 +46,10 @@ class HttpServer { this.app = express(); + if (process.env.NODE_ENV === 'production') { + this.app.set('trust proxy', true); + } + this.configureHelmet(httpConfig); this.o2TokenService = new O2TokenService(jwtConfig); From c1ce3cdb616fb09cf6ea43aefa7b3e232befbf70 Mon Sep 17 00:00:00 2001 From: George Raduta Date: Mon, 20 Apr 2026 15:49:07 +0200 Subject: [PATCH 3/3] Use void instead of function for eslint warnings --- Framework/Backend/http/server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Framework/Backend/http/server.js b/Framework/Backend/http/server.js index 7c975e8e1..f55090bb8 100644 --- a/Framework/Backend/http/server.js +++ b/Framework/Backend/http/server.js @@ -314,7 +314,7 @@ class HttpServer { * Adds POST route using express router, the path will be prefix with "/api" * By default verifies JWT token unless public options is provided * @param {string} path - path that the callback will be bound to - * @param {function} callbacks - method that handles request and response: function(req, res); + * @param {void} callbacks - method that handles request and response: function(req, res); * token should be passed as req.query.token; * more on req: https://expressjs.com/en/api.html#req * more on res: https://expressjs.com/en/api.html#res @@ -329,7 +329,7 @@ class HttpServer { * Adds PUT route using express router, the path will be prefix with "/api" * By default verifies JWT token unless public options is provided * @param {string} path - path that the callback will be bound to - * @param {function} callbacks - method that handles request and response: function(req, res); + * @param {void} callbacks - method that handles request and response: function(req, res); * token should be passed as req.query.token; * more on req: https://expressjs.com/en/api.html#req * more on res: https://expressjs.com/en/api.html#res @@ -344,7 +344,7 @@ class HttpServer { * Adds PATCH route using express router, the path will be prefix with "/api" * By default verifies JWT token unless public options is provided * @param {string} path - path that the callback will be bound to - * @param {function} callbacks - method that handles request and response: function(req, res); + * @param {void} callbacks - method that handles request and response: function(req, res); * token should be passed as req.query.token; * more on req: https://expressjs.com/en/api.html#req * more on res: https://expressjs.com/en/api.html#res @@ -359,7 +359,7 @@ class HttpServer { * Adds DELETE route using express router, the path will be prefix with "/api" * By default verifies JWT token unless public options is provided * @param {string} path - path that the callback will be bound to - * @param {function} callbacks - method that handles request and response: function(req, res); + * @param {void} callbacks - method that handles request and response: function(req, res); * token should be passed as req.query.token; * more on req: https://expressjs.com/en/api.html#req * more on res: https://expressjs.com/en/api.html#res @@ -556,7 +556,7 @@ class HttpServer { * @todo use promises or generators to call it asynchronously! * @param {object} req - HTTP request * @param {object} res - HTTP response - * @param {function} next - passes control to next matching route + * @param {void} next - passes control to next matching route */ jwtVerify(req, res, next) { try {