From 8d232b587636a7a5cd18222b872c375f25336ea3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 14:11:28 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20secure=20hi?= =?UTF-8?q?gh-fidelity=20rewards=20data=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit hardens the L10 Token Engine service by: - Integrating helmet() middleware for standard security headers. - Implementing authenticateToken middleware to enforce JWT-based security. - Securing the /data/training/rewards endpoint with both authentication and authorization checks. - Preventing unverified global data enumeration by restricting access to administrative tokens (those without a fleet_id). - Ensuring the service fails securely if JWT_SECRET is not configured. Co-authored-by: dcplatforms <10982057+dcplatforms@users.noreply.github.com> --- package-lock.json | 22 ++++-- services/10-token-engine/index.js | 35 ++++++++- services/10-token-engine/package.json | 5 +- .../tests/security_hardening.test.js | 73 +++++++++++++++++++ 4 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 services/10-token-engine/tests/security_hardening.test.js diff --git a/package-lock.json b/package-lock.json index bebbff881..7813e0926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10341,12 +10341,15 @@ } }, "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.2.0.tgz", + "integrity": "sha512-DRgTIUgnWcJ62KyarxxziuqYxKGnR6Rgg19BlbucN/dpmJbl1XOit6qvoOX0ZT+HhWe5OUVhU/a1zpGyc1xA0Q==", "license": "MIT", "engines": { "node": ">=18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/EvanHahn" } }, "node_modules/hermes-estree": { @@ -19416,7 +19419,7 @@ }, "services/02-grid-signal": { "name": "grid-signal", - "version": "2.5.0", + "version": "2.5.1", "dependencies": { "ajv": "^8.12.0", "dotenv": "^16.3.1", @@ -19551,7 +19554,7 @@ }, "services/04-market-gateway": { "name": "@migrid/market-gateway", - "version": "3.8.4", + "version": "3.8.5", "license": "Apache-2.0", "dependencies": { "axios": "^1.6.0", @@ -19603,7 +19606,7 @@ }, "services/07-device-gateway": { "name": "device-gateway", - "version": "5.7.0", + "version": "5.8.0", "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", @@ -19681,7 +19684,7 @@ }, "services/10-token-engine": { "name": "@migrid/token-engine", - "version": "4.3.4", + "version": "4.3.5", "dependencies": { "axios": "^1.6.0", "decimal.js": "^10.4.3", @@ -19691,7 +19694,10 @@ "redis": "^4.6.10" }, "devDependencies": { - "jest": "^29.7.0" + "helmet": "^8.2.0", + "jest": "^29.7.0", + "jsonwebtoken": "^9.0.3", + "supertest": "^6.3.4" } }, "services/11-ml-engine": { diff --git a/services/10-token-engine/index.js b/services/10-token-engine/index.js index 9f306754f..b46213a9f 100644 --- a/services/10-token-engine/index.js +++ b/services/10-token-engine/index.js @@ -2,11 +2,17 @@ const { Client } = require('pg'); const { Kafka } = require('kafkajs'); const axios = require('axios'); const express = require('express'); +const helmet = require('helmet'); +const jwt = require('jsonwebtoken'); const Decimal = require('decimal.js'); const redis = require('redis'); const app = express(); +app.use(helmet()); +app.use(express.json()); + const port = process.env.PORT || 3010; +const JWT_SECRET = process.env.JWT_SECRET; const pgClient = new Client({ connectionString: process.env.DATABASE_URL }); const kafka = new Kafka({ @@ -21,6 +27,25 @@ const redisClient = redis.createClient({ const consumer = kafka.consumer({ groupId: 'token-engine-group' }); +// Middleware: Verify JWT token +const authenticateToken = (req, res, next) => { + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; + + if (!token) return res.status(401).json({ error: 'Access token required' }); + + if (!JWT_SECRET) { + console.error('[Security] JWT_SECRET is not configured. Rejecting authentication.'); + return res.status(500).json({ error: 'Internal server error' }); + } + + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) return res.status(403).json({ error: 'Invalid or expired token' }); + req.user = user; + next(); + }); +}; + // Thresholds for Dynamic Multipliers (surplus/scarcity) - Configurable via ENV const LMP_THRESHOLD_SURPLUS = new Decimal(process.env.LMP_THRESHOLD_SURPLUS || '30.0'); const LMP_THRESHOLD_SCARCITY = new Decimal(process.env.LMP_THRESHOLD_SCARCITY || '100.0'); @@ -150,9 +175,17 @@ app.get('/health', (req, res) => { * [Phase 6 AI Readiness] * GET /data/training/rewards * Exposes high-fidelity reward data for L11 ML Engine training. + * Security: Restricted to admin/system tokens (no fleet_id). */ -app.get('/data/training/rewards', async (req, res) => { +app.get('/data/training/rewards', authenticateToken, async (req, res) => { const { site_id, limit = 100 } = req.query; + + // Authorization: Only admin/system tokens (without fleet_id) can access global training data + if (req.user.fleet_id) { + console.warn(`[Security] Unauthorized global rewards data export attempt by fleet_id: ${req.user.fleet_id}`); + return res.status(403).json({ error: 'Forbidden: Unauthorized access to global training data.' }); + } + try { let query = 'SELECT * FROM token_reward_log WHERE is_sentinel_fidelity = TRUE'; const params = []; diff --git a/services/10-token-engine/package.json b/services/10-token-engine/package.json index 42706489f..228854c93 100644 --- a/services/10-token-engine/package.json +++ b/services/10-token-engine/package.json @@ -14,6 +14,9 @@ "test": "jest" }, "devDependencies": { - "jest": "^29.7.0" + "helmet": "^8.2.0", + "jest": "^29.7.0", + "jsonwebtoken": "^9.0.3", + "supertest": "^6.3.4" } } diff --git a/services/10-token-engine/tests/security_hardening.test.js b/services/10-token-engine/tests/security_hardening.test.js new file mode 100644 index 000000000..6991c0ac8 --- /dev/null +++ b/services/10-token-engine/tests/security_hardening.test.js @@ -0,0 +1,73 @@ +process.env.JWT_SECRET = 'test_secret'; +const request = require('supertest'); +const jwt = require('jsonwebtoken'); + +// Mock redis before importing app +jest.mock('redis', () => ({ + createClient: jest.fn(() => ({ + connect: jest.fn(), + on: jest.fn(), + hGet: jest.fn(), + hSet: jest.fn(), + get: jest.fn(), + set: jest.fn(), + quit: jest.fn() + })) +})); + +// Mock pg before importing app +jest.mock('pg', () => { + const mClient = { + connect: jest.fn(), + query: jest.fn(), + end: jest.fn(), + on: jest.fn(), + }; + return { Client: jest.fn(() => mClient) }; +}); + +const { app } = require('../index'); + +describe('L10 Token Engine Security Hardening', () => { + test('GET /data/training/rewards should return 401 if no token provided', async () => { + const response = await request(app).get('/data/training/rewards'); + expect(response.status).toBe(401); + }); + + test('GET /data/training/rewards should return 403 if invalid token provided', async () => { + const response = await request(app) + .get('/data/training/rewards') + .set('Authorization', 'Bearer invalid_token'); + expect(response.status).toBe(403); + }); + + test('GET /data/training/rewards should return 403 if token contains fleet_id (non-admin)', async () => { + const token = jwt.sign({ driver_id: 'driver-1', fleet_id: 'fleet-1' }, process.env.JWT_SECRET); + const response = await request(app) + .get('/data/training/rewards') + .set('Authorization', `Bearer ${token}`); + expect(response.status).toBe(403); + expect(response.body.error).toContain('Unauthorized access to global training data'); + }); + + test('GET /data/training/rewards should return 200 (or pass auth) if valid admin token provided', async () => { + const token = jwt.sign({ driver_id: 'admin-1' }, process.env.JWT_SECRET); + + // We expect it to NOT be 401 or 403. It might be 200 or 500 (if DB query fails) + // but the point is it passed the auth middleware. + const response = await request(app) + .get('/data/training/rewards') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).not.toBe(401); + expect(response.status).not.toBe(403); + }); + + test('GET /health should return security headers via helmet', async () => { + const response = await request(app).get('/health'); + expect(response.headers['x-dns-prefetch-control']).toBeDefined(); + expect(response.headers['x-frame-options']).toBeDefined(); + expect(response.headers['strict-transport-security']).toBeDefined(); + expect(response.headers['x-content-type-options']).toBeDefined(); + }); +});