@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.
- package/README.md +105 -8
- package/dist/auth/auth-types.d.ts +40 -0
- package/dist/auth/auth-types.d.ts.map +1 -0
- package/dist/auth/auth-types.js +5 -0
- package/dist/auth/auth-types.js.map +1 -0
- package/dist/auth/dashboard-auth.d.ts +97 -0
- package/dist/auth/dashboard-auth.d.ts.map +1 -0
- package/dist/auth/dashboard-auth.js +319 -0
- package/dist/auth/dashboard-auth.js.map +1 -0
- package/dist/auth/dpop.d.ts +38 -0
- package/dist/auth/dpop.d.ts.map +1 -0
- package/dist/auth/dpop.js +72 -0
- package/dist/auth/dpop.js.map +1 -0
- package/dist/auth/oauth.d.ts +25 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +96 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/redis-session-cache.d.ts +21 -0
- package/dist/auth/redis-session-cache.d.ts.map +1 -0
- package/dist/auth/redis-session-cache.js +74 -0
- package/dist/auth/redis-session-cache.js.map +1 -0
- package/dist/auth/session-cache.d.ts +47 -0
- package/dist/auth/session-cache.d.ts.map +1 -0
- package/dist/auth/session-cache.js +91 -0
- package/dist/auth/session-cache.js.map +1 -0
- package/dist/cli.js +23 -5
- package/dist/cli.js.map +1 -1
- package/dist/database/database-interface.d.ts +17 -0
- package/dist/database/database-interface.d.ts.map +1 -0
- package/dist/database/database-interface.js +2 -0
- package/dist/database/database-interface.js.map +1 -0
- package/dist/database/postgres-db.d.ts +18 -0
- package/dist/database/postgres-db.d.ts.map +1 -0
- package/dist/database/postgres-db.js +118 -0
- package/dist/database/postgres-db.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/policy/policy-watcher.d.ts +24 -0
- package/dist/policy/policy-watcher.d.ts.map +1 -0
- package/dist/policy/policy-watcher.js +68 -0
- package/dist/policy/policy-watcher.js.map +1 -0
- package/dist/policy/shell-tokenizer.d.ts +92 -0
- package/dist/policy/shell-tokenizer.d.ts.map +1 -0
- package/dist/policy/shell-tokenizer.js +300 -0
- package/dist/policy/shell-tokenizer.js.map +1 -0
- package/dist/proxy/http-proxy-server.d.ts +26 -0
- package/dist/proxy/http-proxy-server.d.ts.map +1 -0
- package/dist/proxy/http-proxy-server.js +172 -0
- package/dist/proxy/http-proxy-server.js.map +1 -0
- package/dist/proxy/proxy-manager.d.ts +3 -1
- package/dist/proxy/proxy-manager.d.ts.map +1 -1
- package/dist/proxy/proxy-manager.js +10 -3
- package/dist/proxy/proxy-manager.js.map +1 -1
- package/dist/proxy/proxy-server.d.ts +15 -8
- package/dist/proxy/proxy-server.d.ts.map +1 -1
- package/dist/proxy/proxy-server.js +80 -26
- package/dist/proxy/proxy-server.js.map +1 -1
- package/dist/utils/circuit-breaker.d.ts +29 -0
- package/dist/utils/circuit-breaker.d.ts.map +1 -0
- package/dist/utils/circuit-breaker.js +81 -0
- package/dist/utils/circuit-breaker.js.map +1 -0
- package/dist/utils/dashboard-server.d.ts +19 -0
- package/dist/utils/dashboard-server.d.ts.map +1 -0
- package/dist/utils/dashboard-server.js +258 -0
- package/dist/utils/dashboard-server.js.map +1 -0
- package/dist/utils/metrics.d.ts +17 -0
- package/dist/utils/metrics.d.ts.map +1 -0
- package/dist/utils/metrics.js +79 -0
- package/dist/utils/metrics.js.map +1 -0
- package/dist/utils/mtls-config.d.ts +27 -0
- package/dist/utils/mtls-config.d.ts.map +1 -0
- package/dist/utils/mtls-config.js +82 -0
- package/dist/utils/mtls-config.js.map +1 -0
- package/dist/utils/payload-normalizer.d.ts +62 -0
- package/dist/utils/payload-normalizer.d.ts.map +1 -0
- package/dist/utils/payload-normalizer.js +240 -0
- package/dist/utils/payload-normalizer.js.map +1 -0
- package/dist/utils/policy-auditor.d.ts +24 -0
- package/dist/utils/policy-auditor.d.ts.map +1 -0
- package/dist/utils/policy-auditor.js +58 -0
- package/dist/utils/policy-auditor.js.map +1 -0
- package/dist/utils/redis-rate-limiter.d.ts +22 -0
- package/dist/utils/redis-rate-limiter.d.ts.map +1 -0
- package/dist/utils/redis-rate-limiter.js +61 -0
- package/dist/utils/redis-rate-limiter.js.map +1 -0
- package/dist/utils/structured-logger.d.ts +1 -1
- package/dist/utils/structured-logger.d.ts.map +1 -1
- package/dist/utils/tracing.d.ts +7 -0
- package/dist/utils/tracing.d.ts.map +1 -0
- package/dist/utils/tracing.js +34 -0
- package/dist/utils/tracing.js.map +1 -0
- 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,
|
|
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
|
|
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/ #
|
|
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 #
|
|
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]
|
|
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
|
|
625
|
-
- [ ]
|
|
626
|
-
- [ ] Multi-user proxy
|
|
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 @@
|
|
|
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
|