@mcp-guardian/server 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +105 -8
  2. package/dist/auth/auth-types.d.ts +40 -0
  3. package/dist/auth/auth-types.d.ts.map +1 -0
  4. package/dist/auth/auth-types.js +5 -0
  5. package/dist/auth/auth-types.js.map +1 -0
  6. package/dist/auth/dashboard-auth.d.ts +97 -0
  7. package/dist/auth/dashboard-auth.d.ts.map +1 -0
  8. package/dist/auth/dashboard-auth.js +319 -0
  9. package/dist/auth/dashboard-auth.js.map +1 -0
  10. package/dist/auth/dpop.d.ts +38 -0
  11. package/dist/auth/dpop.d.ts.map +1 -0
  12. package/dist/auth/dpop.js +72 -0
  13. package/dist/auth/dpop.js.map +1 -0
  14. package/dist/auth/oauth.d.ts +25 -0
  15. package/dist/auth/oauth.d.ts.map +1 -0
  16. package/dist/auth/oauth.js +96 -0
  17. package/dist/auth/oauth.js.map +1 -0
  18. package/dist/auth/redis-session-cache.d.ts +21 -0
  19. package/dist/auth/redis-session-cache.d.ts.map +1 -0
  20. package/dist/auth/redis-session-cache.js +74 -0
  21. package/dist/auth/redis-session-cache.js.map +1 -0
  22. package/dist/auth/session-cache.d.ts +47 -0
  23. package/dist/auth/session-cache.d.ts.map +1 -0
  24. package/dist/auth/session-cache.js +91 -0
  25. package/dist/auth/session-cache.js.map +1 -0
  26. package/dist/cli.js +23 -5
  27. package/dist/cli.js.map +1 -1
  28. package/dist/database/database-interface.d.ts +17 -0
  29. package/dist/database/database-interface.d.ts.map +1 -0
  30. package/dist/database/database-interface.js +2 -0
  31. package/dist/database/database-interface.js.map +1 -0
  32. package/dist/database/postgres-db.d.ts +18 -0
  33. package/dist/database/postgres-db.d.ts.map +1 -0
  34. package/dist/database/postgres-db.js +118 -0
  35. package/dist/database/postgres-db.js.map +1 -0
  36. package/dist/index.js +1 -1
  37. package/dist/policy/policy-watcher.d.ts +24 -0
  38. package/dist/policy/policy-watcher.d.ts.map +1 -0
  39. package/dist/policy/policy-watcher.js +68 -0
  40. package/dist/policy/policy-watcher.js.map +1 -0
  41. package/dist/policy/shell-tokenizer.d.ts +92 -0
  42. package/dist/policy/shell-tokenizer.d.ts.map +1 -0
  43. package/dist/policy/shell-tokenizer.js +300 -0
  44. package/dist/policy/shell-tokenizer.js.map +1 -0
  45. package/dist/proxy/http-proxy-server.d.ts +26 -0
  46. package/dist/proxy/http-proxy-server.d.ts.map +1 -0
  47. package/dist/proxy/http-proxy-server.js +172 -0
  48. package/dist/proxy/http-proxy-server.js.map +1 -0
  49. package/dist/proxy/proxy-manager.d.ts +3 -1
  50. package/dist/proxy/proxy-manager.d.ts.map +1 -1
  51. package/dist/proxy/proxy-manager.js +10 -3
  52. package/dist/proxy/proxy-manager.js.map +1 -1
  53. package/dist/proxy/proxy-server.d.ts +15 -8
  54. package/dist/proxy/proxy-server.d.ts.map +1 -1
  55. package/dist/proxy/proxy-server.js +80 -26
  56. package/dist/proxy/proxy-server.js.map +1 -1
  57. package/dist/utils/circuit-breaker.d.ts +29 -0
  58. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  59. package/dist/utils/circuit-breaker.js +81 -0
  60. package/dist/utils/circuit-breaker.js.map +1 -0
  61. package/dist/utils/dashboard-server.d.ts +19 -0
  62. package/dist/utils/dashboard-server.d.ts.map +1 -0
  63. package/dist/utils/dashboard-server.js +258 -0
  64. package/dist/utils/dashboard-server.js.map +1 -0
  65. package/dist/utils/metrics.d.ts +17 -0
  66. package/dist/utils/metrics.d.ts.map +1 -0
  67. package/dist/utils/metrics.js +79 -0
  68. package/dist/utils/metrics.js.map +1 -0
  69. package/dist/utils/mtls-config.d.ts +27 -0
  70. package/dist/utils/mtls-config.d.ts.map +1 -0
  71. package/dist/utils/mtls-config.js +82 -0
  72. package/dist/utils/mtls-config.js.map +1 -0
  73. package/dist/utils/payload-normalizer.d.ts +62 -0
  74. package/dist/utils/payload-normalizer.d.ts.map +1 -0
  75. package/dist/utils/payload-normalizer.js +240 -0
  76. package/dist/utils/payload-normalizer.js.map +1 -0
  77. package/dist/utils/policy-auditor.d.ts +24 -0
  78. package/dist/utils/policy-auditor.d.ts.map +1 -0
  79. package/dist/utils/policy-auditor.js +58 -0
  80. package/dist/utils/policy-auditor.js.map +1 -0
  81. package/dist/utils/redis-rate-limiter.d.ts +22 -0
  82. package/dist/utils/redis-rate-limiter.d.ts.map +1 -0
  83. package/dist/utils/redis-rate-limiter.js +61 -0
  84. package/dist/utils/redis-rate-limiter.js.map +1 -0
  85. package/dist/utils/structured-logger.d.ts +1 -1
  86. package/dist/utils/structured-logger.d.ts.map +1 -1
  87. package/dist/utils/tracing.d.ts +7 -0
  88. package/dist/utils/tracing.d.ts.map +1 -0
  89. package/dist/utils/tracing.js +34 -0
  90. package/dist/utils/tracing.js.map +1 -0
  91. package/package.json +2 -1
package/README.md CHANGED
@@ -22,6 +22,7 @@ MCP Guardian scans your [Model Context Protocol](https://modelcontextprotocol.io
22
22
  - [One-Off Scan](#one-off-scan)
23
23
  - [CLI Reference](#cli-reference)
24
24
  - [`mcp-guardian proxy`](#mcp-guardian-proxy)
25
+ - [Policy Engine (v0.4+)](#policy-engine-v04)
25
26
  - [`mcp-guardian scan`](#mcp-guardian-scan)
26
27
  - [`mcp-guardian audit`](#mcp-guardian-audit)
27
28
  - [`mcp-guardian health`](#mcp-guardian-health)
@@ -30,6 +31,7 @@ MCP Guardian scans your [Model Context Protocol](https://modelcontextprotocol.io
30
31
  - [Available Tools](#available-tools)
31
32
  - [Available Resources & Prompts](#available-resources--prompts)
32
33
  - [CI/CD Integration](#cicd-integration)
34
+ - [Production Deployment (K8s + Helm)](#production-deployment-k8s--helm)
33
35
  - [Docker](#docker)
34
36
  - [Architecture](#architecture)
35
37
  - [Data Flow (Proxy → DB → Audit)](#data-flow-proxy--db--audit)
@@ -38,6 +40,7 @@ MCP Guardian scans your [Model Context Protocol](https://modelcontextprotocol.io
38
40
  - [Pricing Models](#pricing-models)
39
41
  - [Environment Variables](#environment-variables)
40
42
  - [Development](#development)
43
+ - [SECURITY.md](#securitymd)
41
44
  - [FAQ](#faq)
42
45
  - [Roadmap](#roadmap)
43
46
  - [License](#license)
@@ -50,10 +53,12 @@ As MCP adoption grows, so does the attack surface. MCP servers run arbitrary com
50
53
 
51
54
  MCP Guardian provides:
52
55
 
56
+ - **Active policy enforcement (v0.4+)** — YAML-configurable policy engine that blocks, flags, or passes every `tools/call` in real time based on tool allowlists/denylists, regex patterns, rate limits, and token budgets
53
57
  - **Security auditing** — CVE scanning (OSV.dev + NVD), hardcoded secret detection, typo-squatting detection, command injection detection, and TLS validation
54
58
  - **Real cost tracking** — Proxy interceptor that captures actual `tools/call` traffic and counts tokens via `tiktoken` (o200k_base encoding) — no estimates, no mocks
55
59
  - **Health monitoring** — Live JSON-RPC 2.0 handshake probes with latency, success rate, tool count, and context pressure analysis
56
60
  - **Agent-native** — Runs as an MCP server so your AI assistant can self-audit its own infrastructure
61
+ - **Enterprise SIEM logging (v0.4+)** — Structured JSON logs via pino with request-ID tracing, policy decision audit trails, and block events at WARN level
57
62
 
58
63
  ---
59
64
 
@@ -69,6 +74,7 @@ MCP Guardian provides:
69
74
  | **Typo-Squat Detection** | Levenshtein distance matching against 24 known official MCP packages |
70
75
  | **Secret Scanning** | 6 regex patterns for hardcoded API keys, tokens, private keys, passwords, GitHub tokens, OpenAI keys |
71
76
  | **Command Validation** | Flags dangerous patterns (path traversal, shell chaining, `rm -rf`, `curl`/`wget` in commands, and more) |
77
+ | **🔴 Active Policy Engine (v0.4+)** | YAML-configurable rules: tool allowlist/denylist, regex pattern blocking, rate limiting, token budgets. Operates in `audit` (passive), `warn` (flag only), or `block` (active enforcement) modes |
72
78
  | **Scoring** | Weighted 0–100 security score with actionable recommendations |
73
79
 
74
80
  ### 💰 Cost Audit (`audit_costs`)
@@ -99,7 +105,7 @@ MCP Guardian provides:
99
105
  - **Graceful Shutdown** — SIGINT/SIGTERM handlers flush DB and close connections
100
106
  - **Batched DB Writes** — 1s debounced flush reduces I/O by 10x
101
107
  - **Alert Thresholds** — 6 CLI flags with exit codes 1/2 for CI/CD integration
102
- - **GitHub Actions CI** — Node 18/20/22 matrix, 63 tests across 10 suites
108
+ - **GitHub Actions CI** — Node 18/20/22 matrix, 74 tests across 11 suites
103
109
 
104
110
  ---
105
111
 
@@ -192,15 +198,64 @@ mcp-guardian report --format markdown --output audit-report.md
192
198
 
193
199
  ### `mcp-guardian proxy`
194
200
 
195
- Start the MCP proxy interceptor to capture real token usage data. The proxy spawns all stdio MCP servers from config, then bridges stdin/stdout.
201
+ Start the MCP proxy interceptor with optional active policy enforcement.
196
202
 
197
203
  ```bash
204
+ # Audit-only (passive)
198
205
  mcp-guardian proxy --config ./cline_mcp_settings.json
206
+
207
+ # Active blocking with default policy
208
+ mcp-guardian proxy --config ./cline_mcp_settings.json --policy ./default-policy.yaml
209
+
210
+ # Active blocking with custom policy + mode override
211
+ mcp-guardian proxy --config ./cline_mcp_settings.json --policy ./my-policy.yaml --blocking-mode block
199
212
  ```
200
213
 
201
214
  | Option | Description |
202
215
  |---|---|
203
216
  | `-c, --config <path>` | Path to MCP config file |
217
+ | `--policy <path>` | Path to policy YAML file (enables active blocking) |
218
+ | `--blocking-mode <mode>` | Override policy mode: `audit` (passive), `warn` (flag), `block` (enforce) |
219
+
220
+ ### Policy Engine (v0.4+)
221
+
222
+ The policy engine evaluates every intercepted `tools/call` before it reaches the MCP server. Define rules in YAML:
223
+
224
+ ```yaml
225
+ # my-policy.yaml
226
+ version: "1.0"
227
+ policy:
228
+ mode: block
229
+ rules:
230
+ - name: "deny-shell-tools"
231
+ action: block
232
+ tools: { deny: ["execute_command", "bash", "sh", "eval", "exec"] }
233
+ - name: "block-injection"
234
+ action: block
235
+ patterns:
236
+ - "rm\\s+-rf"
237
+ - "curl\\s|wget\\s"
238
+ - ";\\s*\\w"
239
+ - "&&|\\|\\|"
240
+ - name: "rate-limit"
241
+ action: flag
242
+ maxCallsPerMinute: 60
243
+ - name: "token-budget"
244
+ action: flag
245
+ maxTokens: 50000
246
+ ```
247
+
248
+ **Blocked calls** return a JSON-RPC 2.0 error to the client:
249
+ ```json
250
+ {"jsonrpc":"2.0","id":"abc-123","error":{"code":-32001,"message":"Blocked by MCP Guardian policy: Tool 'execute_command' is explicitly denied"}}
251
+ ```
252
+
253
+ **Policy modes:**
254
+ | Mode | Behavior |
255
+ |---|---|
256
+ | `audit` | Pass all calls; log decisions only (passive) |
257
+ | `warn` | Downgrade `block` actions to `flag`; log warnings |
258
+ | `block` | Full active enforcement — blocked calls never reach the MCP server |
204
259
 
205
260
  ### `mcp-guardian scan`
206
261
 
@@ -349,6 +404,41 @@ Run MCP Guardian in CI to catch issues before deployment:
349
404
 
350
405
  ---
351
406
 
407
+ ## Production Deployment (K8s + Helm)
408
+
409
+ See the full guide at **[deploy/PRODUCTION.md](deploy/PRODUCTION.md)**.
410
+
411
+ ### Quick Helm Install
412
+
413
+ ```bash
414
+ # Install from local chart
415
+ helm install mcp-guardian ./deploy/helm/mcp-guardian \
416
+ --set config.policy.mode=block \
417
+ --set config.mcpConfigPath=/etc/mcp-guardian/cline_mcp_settings.json
418
+
419
+ # Or from the repo (future)
420
+ helm repo add mcp-guardian https://rudraneel93.github.io/mcp-guardian
421
+ helm install mcp-guardian mcp-guardian/mcp-guardian
422
+ ```
423
+
424
+ ### Key Features
425
+ - **Helm chart** with ConfigMap-backed policies, PVC persistence, and safe defaults
426
+ - **Fail-closed** by default (block traffic if proxy crashes) — configurable to fail-open
427
+ - **Sidecar injection pattern** documented for stdio MCP servers
428
+ - **Scaling guide** with CPU/memory recommendations per traffic level
429
+ - **Pod Disruption Budget** for HA, anti-affinity for multi-AZ
430
+ - **SIEM integration** via pino structured JSON logs (Splunk, Datadog, Elasticsearch)
431
+
432
+ ### Performance Overhead
433
+
434
+ | Scenario | p50 | p99 | Overhead |
435
+ |----------|-----|-----|----------|
436
+ | Direct MCP (no proxy) | 5ms | 7ms | — |
437
+ | Proxy (no policy) | 27ms | 77ms | +25.78ms |
438
+ | Proxy (blocking policy) | 27ms | 74ms | +25.93ms |
439
+
440
+ Policy engine adds **~0.15ms** — negligible. The ~26ms is Node.js child process stdio overhead.
441
+
352
442
  ## Docker
353
443
 
354
444
  A Docker image is available for running the proxy in containerized environments.
@@ -414,7 +504,7 @@ mcp-guardian/
414
504
  │ ├── scoring.ts # Shared scoring utility
415
505
  │ └── logger.ts # Colored console logger with log levels
416
506
 
417
- tests/ # 63 tests across 10 suites (Vitest)
507
+ tests/ # 74 tests across 11 suites (Vitest)
418
508
  ├── config-parser.test.ts
419
509
  ├── secret-scanner.test.ts
420
510
  ├── auth-prober.test.ts
@@ -541,7 +631,7 @@ npm install
541
631
  npm run dev # Watch mode with tsx
542
632
  npm run build # Compile TypeScript
543
633
  npm run lint # Type check (tsc --noEmit)
544
- npm test # 63 tests across 10 suites (Vitest)
634
+ npm test # 74 tests across 11 suites (Vitest)
545
635
  npm run test:watch # Watch mode
546
636
 
547
637
  # Contributing
@@ -617,13 +707,20 @@ Token counting uses `tiktoken` with the `o200k_base` encoding (used by GPT-4o an
617
707
  - [x] Token-bucket rate limiter (OSV + NVD)
618
708
  - [x] TLS certificate validation
619
709
  - [x] Command injection validation (10 suspicious patterns)
620
- - [x] 63 unit tests (10 test suites)
710
+ - [x] Active policy engine YAML-based pass/block/flag with allowlists, regex, rate limiting, token budgets
711
+ - [x] Structured JSON logging (pino) for SIEM ingestion
712
+ - [x] STRIDE threat model (SECURITY.md)
713
+ - [x] 74 tests (11 suites)
621
714
  - [x] GitHub Actions CI (Node 18/20/22 matrix)
715
+ - [x] Performance benchmarks (p50: 5ms baseline, +25.78ms proxy overhead, +0.15ms policy)
716
+ - [x] Helm chart + production deployment guide (K8s, fail-open/closed, sidecar pattern, scaling)
622
717
  - [x] Published to npm as [`@mcp-guardian/server`](https://www.npmjs.com/package/@mcp-guardian/server)
718
+ - [ ] OPA integration for Rego policies
719
+ - [ ] OAuth 2.1 / OIDC proxy authentication
623
720
  - [ ] Web dashboard for historical trends
624
- - [ ] Slack/Discord alerting integration
625
- - [ ] Custom CVE feed support
626
- - [ ] Multi-user proxy mode
721
+ - [ ] Slack/Discord alerting
722
+ - [ ] Prometheus metrics endpoint
723
+ - [ ] Multi-user proxy
627
724
 
628
725
  ---
629
726
 
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OAuth 2.1 / OIDC authentication types for MCP Guardian proxy.
3
+ */
4
+ export interface AuthConfig {
5
+ /** OIDC issuer URL (e.g., https://accounts.google.com) */
6
+ issuer: string;
7
+ /** Expected audience claim in JWT */
8
+ audience: string;
9
+ /** Whether authentication is required (fail-closed) or optional (fail-open) */
10
+ required: boolean;
11
+ /** JWKS URI override (default: auto-discovered from issuer) */
12
+ jwksUri?: string;
13
+ /** Clock tolerance in seconds for JWT validation */
14
+ clockTolerance?: number;
15
+ }
16
+ export interface AgentIdentity {
17
+ /** Subject claim (sub) — unique agent identifier */
18
+ sub: string;
19
+ /** Client ID from the token */
20
+ clientId?: string;
21
+ /** Scopes granted to this agent */
22
+ scopes?: string[];
23
+ /** Issuer of the token */
24
+ issuer: string;
25
+ /** Token expiry timestamp */
26
+ expiresAt?: number;
27
+ }
28
+ export interface AuthValidationResult {
29
+ valid: boolean;
30
+ identity?: AgentIdentity;
31
+ error?: string;
32
+ }
33
+ export interface OIDCDiscovery {
34
+ issuer: string;
35
+ jwks_uri: string;
36
+ authorization_endpoint?: string;
37
+ token_endpoint?: string;
38
+ scopes_supported?: string[];
39
+ }
40
+ //# sourceMappingURL=auth-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-types.d.ts","sourceRoot":"","sources":["../../src/auth/auth-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,QAAQ,EAAE,OAAO,CAAC;IAClB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,0BAA0B;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * OAuth 2.1 / OIDC authentication types for MCP Guardian proxy.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=auth-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-types.js","sourceRoot":"","sources":["../../src/auth/auth-types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,97 @@
1
+ export interface AuthResult {
2
+ authenticated: boolean;
3
+ reason?: string;
4
+ identity?: string;
5
+ }
6
+ export interface DashboardAuthConfig {
7
+ /** Enable authentication on dashboard API */
8
+ enabled: boolean;
9
+ /** Pre-shared API key (simplest auth) */
10
+ apiKey?: string;
11
+ /** JWT HMAC secret for session tokens */
12
+ jwtSecret?: string;
13
+ /** Session token expiry in seconds */
14
+ sessionTtlSeconds: number;
15
+ /** Allowed origins for CORS/CSRF validation */
16
+ allowedOrigins: string[];
17
+ /** Maximum login attempts per minute per IP */
18
+ maxLoginAttemptsPerMinute: number;
19
+ }
20
+ /**
21
+ * DashboardAuth provides authentication for the dashboard HTTP server.
22
+ *
23
+ * Two modes:
24
+ * 1. API Key: Set DASHBOARD_API_KEY, pass as ?api_key=<key> or Authorization: Bearer <key>
25
+ * 2. JWT Sessions: Set DASHBOARD_JWT_SECRET, POST /api/login with credentials
26
+ */
27
+ export declare class DashboardAuth {
28
+ private config;
29
+ private loginRateMap;
30
+ private activeTokens;
31
+ private cleanupInterval;
32
+ constructor(config?: Partial<DashboardAuthConfig>);
33
+ /**
34
+ * Authenticate a dashboard HTTP request.
35
+ * Checks multiple sources:
36
+ * 1. ?api_key=<key> query parameter
37
+ * 2. Authorization: Bearer <token> header
38
+ * 3. X-API-Key: <key> header
39
+ */
40
+ authenticate(req: {
41
+ url?: string;
42
+ headers?: Record<string, string | string[] | undefined>;
43
+ method?: string;
44
+ }): AuthResult;
45
+ /**
46
+ * Handle a login attempt. Creates a session token if credentials are valid.
47
+ * Credentials are validated against DASHBOARD_USERNAME / DASHBOARD_PASSWORD env vars.
48
+ */
49
+ login(req: {
50
+ url?: string;
51
+ headers?: Record<string, string | string[] | undefined>;
52
+ body?: {
53
+ username?: string;
54
+ password?: string;
55
+ api_key?: string;
56
+ };
57
+ ip?: string;
58
+ }): {
59
+ success: boolean;
60
+ token?: string;
61
+ error?: string;
62
+ };
63
+ /**
64
+ * Revoke a session token (logout).
65
+ */
66
+ logout(token: string): void;
67
+ /**
68
+ * Generate login page HTML (serves at /login when JWT auth is enabled).
69
+ */
70
+ getLoginPageHtml(error?: string): string;
71
+ /**
72
+ * Check if auth is enabled and required.
73
+ */
74
+ isEnabled(): boolean;
75
+ /**
76
+ * Check if JWT session-based auth is configured (vs API key only).
77
+ */
78
+ hasJwtSessionAuth(): boolean;
79
+ /**
80
+ * Create a signed HMAC session token.
81
+ */
82
+ private createSessionToken;
83
+ /**
84
+ * Timing-safe string comparison to prevent timing attacks on API keys.
85
+ */
86
+ private timingSafeCompare;
87
+ /**
88
+ * Validate CSRF protection via Origin/Referer headers.
89
+ */
90
+ private validateCsrf;
91
+ private isAllowedOrigin;
92
+ private checkLoginRate;
93
+ private normalizeHeaders;
94
+ private cleanupRateMap;
95
+ dispose(): void;
96
+ }
97
+ //# sourceMappingURL=dashboard-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-auth.d.ts","sourceRoot":"","sources":["../../src/auth/dashboard-auth.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,+CAA+C;IAC/C,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,+CAA+C;IAC/C,yBAAyB,EAAE,MAAM,CAAC;CACnC;AAUD;;;;;;GAMG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,YAAY,CAA0C;IAC9D,OAAO,CAAC,YAAY,CAA0B;IAC9C,OAAO,CAAC,eAAe,CAA+C;gBAE1D,MAAM,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC;IA4BjD;;;;;;OAMG;IACH,YAAY,CAAC,GAAG,EAAE;QAChB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;QACxD,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,UAAU;IAyDd;;;OAGG;IACH,KAAK,CAAC,GAAG,EAAE;QACT,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;QACxD,IAAI,CAAC,EAAE;YAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;YAAC,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAClE,EAAE,CAAC,EAAE,MAAM,CAAC;KACb,GAAG;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAuDxD;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI3B;;OAEG;IACH,gBAAgB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM;IAyCxC;;OAEG;IACH,SAAS,IAAI,OAAO;IAIpB;;OAEG;IACH,iBAAiB,IAAI,OAAO;IAI5B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmB1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAOzB;;OAEG;IACH,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,cAAc;IAOtB,OAAO,IAAI,IAAI;CAKhB"}
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Dashboard Authentication Middleware
3
+ *
4
+ * Provides JWT-based authentication for the dashboard HTTP API.
5
+ * Supports:
6
+ * - API key authentication (simple, internal deployments)
7
+ * - JWT session tokens (for multi-user deployments)
8
+ * - CSRF protection via Origin/Referer validation
9
+ * - Rate limiting on auth endpoints
10
+ * - Login endpoint with configurable credential source
11
+ *
12
+ * Enable with: DASHBOARD_AUTH_ENABLED=true
13
+ * Configure API key: DASHBOARD_API_KEY=<key>
14
+ * Configure JWT secret: DASHBOARD_JWT_SECRET=<secret>
15
+ */
16
+ import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
17
+ import { Logger } from '../utils/logger.js';
18
+ import { StructuredLogger } from '../utils/structured-logger.js';
19
+ /**
20
+ * DashboardAuth provides authentication for the dashboard HTTP server.
21
+ *
22
+ * Two modes:
23
+ * 1. API Key: Set DASHBOARD_API_KEY, pass as ?api_key=<key> or Authorization: Bearer <key>
24
+ * 2. JWT Sessions: Set DASHBOARD_JWT_SECRET, POST /api/login with credentials
25
+ */
26
+ export class DashboardAuth {
27
+ config;
28
+ loginRateMap = new Map();
29
+ activeTokens = new Set();
30
+ cleanupInterval = null;
31
+ constructor(config) {
32
+ const enabled = process.env['DASHBOARD_AUTH_ENABLED'] === 'true';
33
+ this.config = {
34
+ enabled: config?.enabled ?? enabled,
35
+ apiKey: config?.apiKey ?? process.env['DASHBOARD_API_KEY'] ?? undefined,
36
+ jwtSecret: config?.jwtSecret ?? process.env['DASHBOARD_JWT_SECRET'] ?? undefined,
37
+ sessionTtlSeconds: config?.sessionTtlSeconds ?? 3600,
38
+ allowedOrigins: config?.allowedOrigins ?? (process.env['DASHBOARD_ALLOWED_ORIGINS']
39
+ ? process.env['DASHBOARD_ALLOWED_ORIGINS'].split(',').map(s => s.trim())
40
+ : ['http://localhost:4000', 'http://localhost:3000', 'http://127.0.0.1:4000']),
41
+ maxLoginAttemptsPerMinute: config?.maxLoginAttemptsPerMinute ?? 5,
42
+ };
43
+ if (this.config.enabled && this.config.apiKey) {
44
+ Logger.info('[dashboard-auth] API key authentication enabled');
45
+ }
46
+ else if (this.config.enabled && this.config.jwtSecret) {
47
+ Logger.info('[dashboard-auth] JWT session authentication enabled');
48
+ }
49
+ else if (this.config.enabled) {
50
+ Logger.warn('[dashboard-auth] Auth enabled but no API key or JWT secret configured');
51
+ }
52
+ // Periodic cleanup of rate limit entries
53
+ if (this.config.enabled) {
54
+ this.cleanupInterval = setInterval(() => this.cleanupRateMap(), 60000);
55
+ }
56
+ }
57
+ /**
58
+ * Authenticate a dashboard HTTP request.
59
+ * Checks multiple sources:
60
+ * 1. ?api_key=<key> query parameter
61
+ * 2. Authorization: Bearer <token> header
62
+ * 3. X-API-Key: <key> header
63
+ */
64
+ authenticate(req) {
65
+ if (!this.config.enabled) {
66
+ return { authenticated: true, identity: 'anonymous' };
67
+ }
68
+ const url = req.url || '/';
69
+ const headers = this.normalizeHeaders(req.headers || {});
70
+ // ── CSRF check for mutating requests ──
71
+ if (req.method && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) {
72
+ const csrfResult = this.validateCsrf(headers);
73
+ if (!csrfResult.authenticated)
74
+ return csrfResult;
75
+ }
76
+ // ── Check query param API key ──
77
+ try {
78
+ const urlObj = new URL(url, 'http://localhost');
79
+ const queryKey = urlObj.searchParams.get('api_key');
80
+ if (queryKey && this.config.apiKey) {
81
+ if (this.timingSafeCompare(queryKey, this.config.apiKey)) {
82
+ return { authenticated: true, identity: 'api_key' };
83
+ }
84
+ }
85
+ }
86
+ catch {
87
+ // Malformed URL — continue to other auth methods
88
+ }
89
+ // ── Check Authorization header ──
90
+ const authHeader = headers['authorization'];
91
+ if (authHeader) {
92
+ const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
93
+ if (bearerMatch) {
94
+ const token = bearerMatch[1];
95
+ // Check if it's the API key
96
+ if (this.config.apiKey && this.timingSafeCompare(token, this.config.apiKey)) {
97
+ return { authenticated: true, identity: 'api_key' };
98
+ }
99
+ // Check if it's a valid session token
100
+ if (this.activeTokens.has(token)) {
101
+ return { authenticated: true, identity: 'session' };
102
+ }
103
+ }
104
+ }
105
+ // ── Check X-API-Key header ──
106
+ const apiKeyHeader = headers['x-api-key'];
107
+ if (apiKeyHeader && this.config.apiKey) {
108
+ if (this.timingSafeCompare(apiKeyHeader, this.config.apiKey)) {
109
+ return { authenticated: true, identity: 'api_key' };
110
+ }
111
+ }
112
+ return { authenticated: false, reason: 'No valid authentication provided' };
113
+ }
114
+ /**
115
+ * Handle a login attempt. Creates a session token if credentials are valid.
116
+ * Credentials are validated against DASHBOARD_USERNAME / DASHBOARD_PASSWORD env vars.
117
+ */
118
+ login(req) {
119
+ if (!this.config.enabled || !this.config.jwtSecret) {
120
+ return { success: false, error: 'JWT auth not configured. Set DASHBOARD_JWT_SECRET.' };
121
+ }
122
+ // ── Rate limit login attempts ──
123
+ const ip = req.ip || 'unknown';
124
+ if (!this.checkLoginRate(ip)) {
125
+ StructuredLogger.info({
126
+ event: 'dashboard_login_rate_limited',
127
+ ip,
128
+ });
129
+ return { success: false, error: 'Too many login attempts. Try again later.' };
130
+ }
131
+ const body = req.body || {};
132
+ // Check API key shortcut
133
+ if (body.api_key && this.config.apiKey && this.timingSafeCompare(body.api_key, this.config.apiKey)) {
134
+ const token = this.createSessionToken();
135
+ Logger.info(`[dashboard-auth] Login via API key from ${ip}`);
136
+ return { success: true, token };
137
+ }
138
+ // Check username/password
139
+ const expectedUsername = process.env['DASHBOARD_USERNAME'];
140
+ const expectedPassword = process.env['DASHBOARD_PASSWORD'];
141
+ if (!expectedUsername || !expectedPassword) {
142
+ return { success: false, error: 'Login credentials not configured on server. Set DASHBOARD_USERNAME and DASHBOARD_PASSWORD.' };
143
+ }
144
+ if (body.username === expectedUsername &&
145
+ body.password &&
146
+ this.timingSafeCompare(body.password, expectedPassword)) {
147
+ const token = this.createSessionToken();
148
+ StructuredLogger.info({
149
+ event: 'dashboard_login',
150
+ ip,
151
+ identity: body.username,
152
+ });
153
+ return { success: true, token };
154
+ }
155
+ StructuredLogger.info({
156
+ event: 'dashboard_login_failed',
157
+ ip,
158
+ identity: body.username || 'unknown',
159
+ });
160
+ return { success: false, error: 'Invalid credentials' };
161
+ }
162
+ /**
163
+ * Revoke a session token (logout).
164
+ */
165
+ logout(token) {
166
+ this.activeTokens.delete(token);
167
+ }
168
+ /**
169
+ * Generate login page HTML (serves at /login when JWT auth is enabled).
170
+ */
171
+ getLoginPageHtml(error) {
172
+ const errorHtml = error ? `<div style="color:#f85149;margin-bottom:16px;padding:8px;background:#3d1f1f;border-radius:6px;">${error}</div>` : '';
173
+ return `<!DOCTYPE html>
174
+ <html lang="en">
175
+ <head>
176
+ <meta charset="UTF-8">
177
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
178
+ <title>MCP Guardian — Login</title>
179
+ <style>
180
+ * { margin: 0; padding: 0; box-sizing: border-box; }
181
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
182
+ .container { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 32px; width: 100%; max-width: 400px; }
183
+ h1 { font-size: 20px; color: #58a6ff; margin-bottom: 8px; }
184
+ h2 { font-size: 14px; color: #8b949e; margin-bottom: 24px; }
185
+ label { display: block; font-size: 13px; color: #8b949e; margin-bottom: 4px; margin-top: 12px; }
186
+ input { width: 100%; padding: 8px 12px; background: #0d1117; border: 1px solid #30363d; border-radius: 6px; color: #c9d1d9; font-size: 14px; }
187
+ input:focus { outline: none; border-color: #58a6ff; }
188
+ button { width: 100%; padding: 10px; background: #238636; color: #fff; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; margin-top: 20px; }
189
+ button:hover { background: #2ea043; }
190
+ .footer { font-size: 12px; color: #8b949e; margin-top: 16px; text-align: center; }
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <div class="container">
195
+ <h1>🛡️ MCP Guardian</h1>
196
+ <h2>Dashboard Authentication</h2>
197
+ ${errorHtml}
198
+ <form method="POST" action="/api/login">
199
+ <label for="username">Username</label>
200
+ <input type="text" id="username" name="username" required autofocus>
201
+ <label for="password">Password</label>
202
+ <input type="password" id="password" name="password" required>
203
+ <button type="submit">Sign In</button>
204
+ </form>
205
+ <div class="footer">Internal deployment — authorized access only</div>
206
+ </div>
207
+ </body>
208
+ </html>`;
209
+ }
210
+ /**
211
+ * Check if auth is enabled and required.
212
+ */
213
+ isEnabled() {
214
+ return this.config.enabled && !!(this.config.apiKey || this.config.jwtSecret);
215
+ }
216
+ /**
217
+ * Check if JWT session-based auth is configured (vs API key only).
218
+ */
219
+ hasJwtSessionAuth() {
220
+ return this.config.enabled && !!this.config.jwtSecret;
221
+ }
222
+ /**
223
+ * Create a signed HMAC session token.
224
+ */
225
+ createSessionToken() {
226
+ const payload = Buffer.from(JSON.stringify({
227
+ iat: Math.floor(Date.now() / 1000),
228
+ jti: randomBytes(16).toString('hex'),
229
+ })).toString('base64url');
230
+ const signature = createHmac('sha256', this.config.jwtSecret || randomBytes(32).toString('hex'))
231
+ .update(payload)
232
+ .digest('base64url');
233
+ const token = `${payload}.${signature}`;
234
+ this.activeTokens.add(token);
235
+ // Auto-expire after TTL
236
+ setTimeout(() => this.activeTokens.delete(token), this.config.sessionTtlSeconds * 1000);
237
+ return token;
238
+ }
239
+ /**
240
+ * Timing-safe string comparison to prevent timing attacks on API keys.
241
+ */
242
+ timingSafeCompare(a, b) {
243
+ if (a.length !== b.length)
244
+ return false;
245
+ const bufA = Buffer.from(a, 'utf-8');
246
+ const bufB = Buffer.from(b, 'utf-8');
247
+ return timingSafeEqual(bufA, bufB);
248
+ }
249
+ /**
250
+ * Validate CSRF protection via Origin/Referer headers.
251
+ */
252
+ validateCsrf(headers) {
253
+ const origin = headers['origin'];
254
+ const referer = headers['referer'];
255
+ // If both are missing and we're strict, could block
256
+ // For now, only validate when present
257
+ if (origin) {
258
+ if (!this.isAllowedOrigin(origin)) {
259
+ return { authenticated: false, reason: `Origin '${origin}' not allowed` };
260
+ }
261
+ }
262
+ if (referer) {
263
+ try {
264
+ const refererOrigin = new URL(referer).origin;
265
+ if (!this.isAllowedOrigin(refererOrigin)) {
266
+ return { authenticated: false, reason: `Referer origin '${refererOrigin}' not allowed` };
267
+ }
268
+ }
269
+ catch {
270
+ // Malformed referer — allow through
271
+ }
272
+ }
273
+ return { authenticated: true };
274
+ }
275
+ isAllowedOrigin(origin) {
276
+ return this.config.allowedOrigins.some(allowed => {
277
+ if (allowed === '*')
278
+ return true;
279
+ if (allowed === origin)
280
+ return true;
281
+ return false;
282
+ });
283
+ }
284
+ checkLoginRate(ip) {
285
+ const now = Date.now();
286
+ let entry = this.loginRateMap.get(ip);
287
+ if (!entry || now > entry.resetAt) {
288
+ entry = { count: 1, resetAt: now + 60000 };
289
+ this.loginRateMap.set(ip, entry);
290
+ return true;
291
+ }
292
+ entry.count++;
293
+ return entry.count <= this.config.maxLoginAttemptsPerMinute;
294
+ }
295
+ normalizeHeaders(headers) {
296
+ const result = {};
297
+ for (const [key, value] of Object.entries(headers)) {
298
+ if (Array.isArray(value))
299
+ result[key.toLowerCase()] = value[0] || '';
300
+ else if (value !== undefined)
301
+ result[key.toLowerCase()] = value;
302
+ }
303
+ return result;
304
+ }
305
+ cleanupRateMap() {
306
+ const now = Date.now();
307
+ for (const [ip, entry] of this.loginRateMap) {
308
+ if (now > entry.resetAt)
309
+ this.loginRateMap.delete(ip);
310
+ }
311
+ }
312
+ dispose() {
313
+ if (this.cleanupInterval)
314
+ clearInterval(this.cleanupInterval);
315
+ this.activeTokens.clear();
316
+ this.loginRateMap.clear();
317
+ }
318
+ }
319
+ //# sourceMappingURL=dashboard-auth.js.map