Skip to content

Commit 83d2142

Browse files
bogdanmariusc10Bogdan-Marius-Catanusjonpspri
authored
feat(security): implement JWT token security improvements (#4371)
* feat(security): implement JWT token security improvements Implements comprehensive JWT token security enhancements addressing X-Force Red security audit findings: Security Features: - Reduced token lifetime from ~70 days to 20 minutes (configurable 5-1440 min) - Server-side token blocklist with Redis caching and database persistence - Idle timeout enforcement (60 minutes default, configurable) - Logout endpoint with immediate token invalidation - Activity tracking with automatic updates - Token revocation on logout, expiry, and idle timeout - Comprehensive audit logging for security events Implementation: - Added TokenBlocklistService for token revocation management - Enhanced TokenRevocation model with token_expiry and last_activity fields - Added idle timeout checking in get_current_user() - Implemented /auth/logout endpoint with proper dependency injection - Added security configuration with validation (TOKEN_LIFETIME_MINUTES, TOKEN_IDLE_TIMEOUT_MINUTES) - Created Alembic migrations for database schema changes Testing: - 47 tests passing (39 unit + 8 edge cases + 11 integration) - 88% coverage on token_blocklist_service.py - 84% coverage on routers/auth.py - 86% overall coverage for JWT security modules - Comprehensive integration tests for all security flows - Edge case tests for error handling paths Documentation: - Added JWT_TOKEN_SECURITY_IMPLEMENTATION.md with complete implementation guide - Added test_jwt_token_security.md with test documentation - Updated .secrets.baseline for security scanning Closes #4317 Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Signed-off-by: Jonathan Springer <jps@s390x.com> * fix: linting and coverage issues, enhanced /admin/logout to revoke tokens Signed-off-by: Jonathan Springer <jps@s390x.com> * fix: resolve linting issues and pre-commit checks - Remove duplicate datetime/timezone imports in admin.py - Replace __enter__() calls with proper context managers in token_blocklist_service.py - Add pylint disable comments for func.count false positives - Fix dict comprehension to use dict() constructor - Add newline at end of test_jwt_token_security.md Signed-off-by: Jonathan Springer <jps@s390x.com> * test: add unit test for logout SecretStr handling and fix pylint issues - Add test for routers/auth.py line 239 (SecretStr.get_secret_value) - Fix 3 pylint R1705 (no-else-return) violations in token_blocklist_service.py - Update secrets baseline after merge - Add SimpleNamespace import to test_auth.py Improves diff-cover from 90% to 97.4% for routers/auth.py Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Signed-off-by: Jonathan Springer <jps@s390x.com> * fix(security): make idle timeout actually enforce The idle-timeout block in get_current_user only read last_activity from the JWT, but no issuance code wrote it — so the check ran zero times on real tokens. Read activity from Redis first (update_activity was already writing there but had no consumer), fall back to the JWT last_activity claim, fall back to iat. Emit last_activity=iat in create_access_token for first-request bootstrap. Folds in remaining PR-review blockers: - bb43712cae28 alembic merge resolves dual-head from rebase past the head referenced by cae28b15a507 - TOKEN_EXPIRY 10080->20 min default documented in CHANGELOG Behavior Changes with migration & rollback guidance - drop dead refresh_token_expiry config field + 3 doc references - /auth/logout current_user: dict -> EmailUser (matches actual return type of get_current_user) - /admin/logout test rewritten with TestClient + CSRF deny-path regression (was asyncio.run on MagicMock — bypassed middleware and would pass even if the route was unregistered) Coverage on diff: 91% -> ~100%. New unit tests cover every branch of the idle-timeout block plus the Redis-success and fresh-session paths in TokenBlocklistService that were previously unreached. Refs #4317, #4371 Signed-off-by: Jonathan Springer <jps@s390x.com> --------- Signed-off-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Signed-off-by: Jonathan Springer <jps@s390x.com> Co-authored-by: Bogdan-Marius-Catanus <bogdan-marius.catanus@ibm.com> Co-authored-by: Jonathan Springer <jps@s390x.com>
1 parent c87de35 commit 83d2142

22 files changed

Lines changed: 3707 additions & 32 deletions

.secrets.baseline

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "(?x)( package-lock\\.json$ |Cargo\\.lock$ |uv\\.lock$ |go\\.sum$ |mcpgateway/sri_hashes\\.json$ )|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2026-04-26T16:16:40Z",
6+
"generated_at": "2026-04-26T18:36:21Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -250,63 +250,63 @@
250250
"hashed_secret": "b4673e578b9b30fe8bba1b555b7b59883444c697",
251251
"is_secret": false,
252252
"is_verified": false,
253-
"line_number": 845,
253+
"line_number": 859,
254254
"type": "Secret Keyword",
255255
"verified_result": null
256256
},
257257
{
258258
"hashed_secret": "4a0a2df96d4c9a13a282268cab33ac4b8cbb2c72",
259259
"is_secret": false,
260260
"is_verified": false,
261-
"line_number": 933,
261+
"line_number": 947,
262262
"type": "Secret Keyword",
263263
"verified_result": null
264264
},
265265
{
266266
"hashed_secret": "5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8",
267267
"is_secret": false,
268268
"is_verified": false,
269-
"line_number": 1283,
269+
"line_number": 1297,
270270
"type": "Basic Auth Credentials",
271271
"verified_result": null
272272
},
273273
{
274274
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
275275
"is_secret": false,
276276
"is_verified": false,
277-
"line_number": 2649,
277+
"line_number": 2663,
278278
"type": "Basic Auth Credentials",
279279
"verified_result": null
280280
},
281281
{
282282
"hashed_secret": "fa9beb99e4029ad5a6615399e7bbae21356086b3",
283283
"is_secret": false,
284284
"is_verified": false,
285-
"line_number": 2740,
285+
"line_number": 2754,
286286
"type": "Secret Keyword",
287287
"verified_result": null
288288
},
289289
{
290290
"hashed_secret": "ac371b6dcce28a86c90d12bc57d946a800eebf17",
291291
"is_secret": false,
292292
"is_verified": false,
293-
"line_number": 2783,
293+
"line_number": 2797,
294294
"type": "Secret Keyword",
295295
"verified_result": null
296296
},
297297
{
298298
"hashed_secret": "0b6ec68df700dec4dcd64babd0eda1edccddace1",
299299
"is_secret": false,
300300
"is_verified": false,
301-
"line_number": 2788,
301+
"line_number": 2802,
302302
"type": "Secret Keyword",
303303
"verified_result": null
304304
},
305305
{
306306
"hashed_secret": "4ad6f0082ee224001beb3ca5c3e81c8ceea5ed86",
307307
"is_secret": false,
308308
"is_verified": false,
309-
"line_number": 2793,
309+
"line_number": 2807,
310310
"type": "Secret Keyword",
311311
"verified_result": null
312312
}
@@ -4196,23 +4196,23 @@
41964196
"hashed_secret": "559b05f1b2863e725b76e216ac3dadecbf92e244",
41974197
"is_secret": false,
41984198
"is_verified": false,
4199-
"line_number": 4791,
4199+
"line_number": 4825,
42004200
"type": "Secret Keyword",
42014201
"verified_result": null
42024202
},
42034203
{
42044204
"hashed_secret": "a8af4759392d4f7496d613174f33afe2074a4b8d",
42054205
"is_secret": false,
42064206
"is_verified": false,
4207-
"line_number": 4793,
4207+
"line_number": 4827,
42084208
"type": "Secret Keyword",
42094209
"verified_result": null
42104210
},
42114211
{
42124212
"hashed_secret": "85b60d811d16ff56b3654587d4487f713bfa33b7",
42134213
"is_secret": false,
42144214
"is_verified": false,
4215-
"line_number": 15172,
4215+
"line_number": 15206,
42164216
"type": "Secret Keyword",
42174217
"verified_result": null
42184218
}
@@ -4862,7 +4862,7 @@
48624862
"hashed_secret": "ff37a98a9963d347e9749a5c1b3936a4a245a6ff",
48634863
"is_secret": false,
48644864
"is_verified": false,
4865-
"line_number": 2413,
4865+
"line_number": 2420,
48664866
"type": "Secret Keyword",
48674867
"verified_result": null
48684868
}
@@ -4964,7 +4964,7 @@
49644964
"hashed_secret": "aa1a82fe15c74459f1261961b07ae924e2b94ab2",
49654965
"is_secret": false,
49664966
"is_verified": false,
4967-
"line_number": 150,
4967+
"line_number": 151,
49684968
"type": "Secret Keyword",
49694969
"verified_result": null
49704970
}
@@ -4974,23 +4974,23 @@
49744974
"hashed_secret": "6993a3fd94a012ab50fb6b9e97ec238310f0b177",
49754975
"is_secret": false,
49764976
"is_verified": false,
4977-
"line_number": 397,
4977+
"line_number": 401,
49784978
"type": "Secret Keyword",
49794979
"verified_result": null
49804980
},
49814981
{
49824982
"hashed_secret": "52dcc83ec1e54426ad58a64854d1eb8d5f5d9685",
49834983
"is_secret": false,
49844984
"is_verified": false,
4985-
"line_number": 398,
4985+
"line_number": 402,
49864986
"type": "Secret Keyword",
49874987
"verified_result": null
49884988
},
49894989
{
49904990
"hashed_secret": "a616a64c0fbc30f12287d0f24f3b90dd2e6a206e",
49914991
"is_secret": false,
49924992
"is_verified": false,
4993-
"line_number": 680,
4993+
"line_number": 684,
49944994
"type": "Secret Keyword",
49954995
"verified_result": null
49964996
}
@@ -7162,15 +7162,15 @@
71627162
"hashed_secret": "6eb67d95dba1a614971e31e78146d44bd4a3ada3",
71637163
"is_secret": false,
71647164
"is_verified": false,
7165-
"line_number": 191,
7165+
"line_number": 192,
71667166
"type": "Secret Keyword",
71677167
"verified_result": null
71687168
},
71697169
{
71707170
"hashed_secret": "cbfdac6008f9cab4083784cbd1874f76618d2a97",
71717171
"is_secret": false,
71727172
"is_verified": false,
7173-
"line_number": 271,
7173+
"line_number": 272,
71747174
"type": "Secret Keyword",
71757175
"verified_result": null
71767176
}
@@ -9134,15 +9134,15 @@
91349134
"hashed_secret": "516b9783fca517eecbd1d064da2d165310b19759",
91359135
"is_secret": false,
91369136
"is_verified": false,
9137-
"line_number": 1016,
9137+
"line_number": 1017,
91389138
"type": "Basic Auth Credentials",
91399139
"verified_result": null
91409140
},
91419141
{
91429142
"hashed_secret": "ef4eb24299c517306652ffee61e05934f2224914",
91439143
"is_secret": false,
91449144
"is_verified": false,
9145-
"line_number": 1268,
9145+
"line_number": 1269,
91469146
"type": "Secret Keyword",
91479147
"verified_result": null
91489148
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,23 @@
99
- **🛡️ Content Security – Malicious Pattern Detection (US-3)** ([#4072](https://github.com/IBM/mcp-context-forge/pull/4072), [#538](https://github.com/IBM/mcp-context-forge/issues/538)) – Regex-based scanning for XSS, SQL injection, command injection, and template-injection patterns. Applied on the single **and** bulk create/update paths for resources, prompts, and tools (tool `name`, `description`, and JSON-serialized `inputSchema`). New config: `CONTENT_PATTERN_DETECTION_ENABLED`, `CONTENT_BLOCKED_PATTERNS`, `CONTENT_PATTERN_VALIDATION_MODE` (`strict` | `moderate` | `lenient`). Lenient mode logs every matched pattern in a payload (was: only the first).
1010
- **🔒 Content Security – Prompt Template Validation (US-4)** ([#4072](https://github.com/IBM/mcp-context-forge/pull/4072), [#538](https://github.com/IBM/mcp-context-forge/issues/538)) – Pre-render validation of prompt templates: balanced-brace check, Jinja2 syntax check, and dangerous-pattern scan (`__import__`, `eval(`, dunders, etc.). New config: `CONTENT_VALIDATE_PROMPT_TEMPLATES`, `CONTENT_BLOCKED_TEMPLATE_PATTERNS`.
1111
- **⚡ ReDoS Defense for Pattern Scanning** ([#4072](https://github.com/IBM/mcp-context-forge/pull/4072)) – `CONTENT_PATTERN_MAX_SCAN_SIZE` (default 200 KB) caps scan input length deterministically; `CONTENT_PATTERN_REGEX_TIMEOUT` (default 1.0 s) per-pattern. Patterns are pre-compiled once at service init instead of re-compiled per request.
12+
- **🔐 JWT Token Security – Server-Side Revocation, Idle Timeout, Logout** ([#4371](https://github.com/IBM/mcp-context-forge/pull/4371), [#4317](https://github.com/IBM/mcp-context-forge/issues/4317)) – New `TokenBlocklistService` (Redis-cached, DB-persisted) for immediate JWT invalidation. Idle-timeout enforcement on every authenticated request, with activity tracking in Redis (falls back to JWT `iat` when Redis is unavailable). New `POST /auth/logout` (Bearer auth) and enhanced `POST /admin/logout` (cookie auth) revoke the caller's token in the blocklist before clearing session state. Comprehensive audit-log fields (`security_event`, `security_severity`, `jti`, `reason`) for every revocation/idle-timeout event. New config: `TOKEN_IDLE_TIMEOUT`, `TOKEN_BLOCKLIST_CLEANUP_HOURS`. Addresses X-Force Red audit findings on session-token management.
1213

1314
### ⚠️ Behavior Changes
1415

16+
#### **⏱️ `TOKEN_EXPIRY` default reduced from 10080 minutes (~7 days) to 20 minutes** ([#4371](https://github.com/IBM/mcp-context-forge/pull/4371), [#4317](https://github.com/IBM/mcp-context-forge/issues/4317))
17+
18+
**Impact**: Any deployment that does not set `TOKEN_EXPIRY` explicitly will now issue session tokens that expire after **20 minutes** instead of ~7 days. Existing tokens already in circulation are unaffected (they retain the `exp` claim baked in at issuance), but every newly-issued token after upgrade has the shorter lifetime. Automation that re-uses a single login token for hours or days will start receiving HTTP 401 mid-flight.
19+
20+
**Why**: Short-lived tokens are the primary mitigation for stolen-token replay, per the X-Force Red security audit (#4317). 7-day session tokens were previously called out as a finding. The new default brings the gateway in line with industry guidance (5–20 minutes for session tokens).
21+
22+
**Migration**:
23+
- **Interactive sessions** — no action needed; the new `/auth/logout` endpoint and idle-timeout enforcement (60 min default) work transparently.
24+
- **CI/automation that needs longer-lived tokens** — set `TOKEN_EXPIRY` explicitly in `.env` (range: 5–1440 minutes), e.g. `TOKEN_EXPIRY=480` for an 8-hour shift, and pair it with `TOKEN_IDLE_TIMEOUT=0` if the workload bursts after long quiet periods.
25+
- **Long-running scripts using `mcpgateway.utils.create_jwt_token --exp <minutes>`** — the `--exp` flag is unaffected (it overrides the default).
26+
27+
**Rollback**: Set `TOKEN_EXPIRY=10080` to restore the previous 7-day default.
28+
1529
#### **🧪 Prompt templates are now rendered in a Jinja2 sandbox** ([#4072](https://github.com/IBM/mcp-context-forge/pull/4072))
1630

1731
**Impact**: `prompt_service` now uses `jinja2.sandbox.SandboxedEnvironment` instead of plain `jinja2.Environment`. Templates that previously reached Python internals at render time will raise `PromptError: sandbox rejected unsafe operation`.

0 commit comments

Comments
 (0)