mcp-auth-middleware 0.1.0__tar.gz

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.
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - JWE token decryption middleware for Starlette/MCP
8
+ - `get_user()` context accessor for authenticated claims
9
+ - Public JWKS endpoint at `/.well-known/jwks.json`
10
+ - CLI: `mcp-auth-middleware generate` — RSA-4096 JWKS key pair generation
11
+ - CLI: `mcp-auth-middleware k8s` — Kubernetes Secret YAML output
12
+ - CLI: `mcp-auth-middleware clean` — secure key deletion
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fachhochschule Südwestfalen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include LICENSE
2
+ include README.md
3
+ include requirements.txt
4
+ include CHANGELOG.md
5
+ recursive-include mcp_auth_middleware *.py py.typed
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-auth-middleware
3
+ Version: 0.1.0
4
+ Summary: JWE authentication middleware for MCP/Starlette applications
5
+ Author: Aurel Bartmann
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fhswf/mcp-auth-middleware
8
+ Project-URL: Documentation, https://github.com/fhswf/mcp-auth-middleware#readme
9
+ Project-URL: Repository, https://github.com/fhswf/mcp-auth-middleware
10
+ Project-URL: Issues, https://github.com/fhswf/mcp-auth-middleware/issues
11
+ Keywords: mcp,jwe,jwks,authentication,middleware,starlette
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Framework :: FastAPI
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Requires-Python: >=3.13
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: python-jose[cryptography]>=3.5.0
23
+ Requires-Dist: starlette>=0.52.1
24
+ Requires-Dist: jwcrypto>=1.5.6
25
+ Dynamic: license-file
26
+
27
+ # mcp-auth-middleware
28
+
29
+ JWE authentication middleware for HTTP MCP servers. Encrypt user data end-to-end so that only your MCP server can read it.
30
+
31
+ ## What it does
32
+
33
+ `mcp-auth-middleware` gives your MCP server two things:
34
+
35
+ 1. **A middleware** that intercepts incoming requests, decrypts a JWE Bearer token, and makes the authenticated user's claims available via `get_user()`.
36
+ 2. **A CLI** (`mcp-auth-middleware`) that generates RSA key pairs in JWKS format, outputs Kubernetes Secret YAML, and securely deletes local keys when you're done.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install mcp-auth-middleware
44
+ ```
45
+
46
+ This installs the library **and** the `mcp-auth-middleware` CLI.
47
+
48
+ ---
49
+
50
+ ## Quick start (local development)
51
+
52
+ ### 1. Generate keys
53
+
54
+ ```bash
55
+ mcp-auth-middleware generate
56
+ ```
57
+
58
+ Output:
59
+
60
+ ```
61
+ Keys generated (JWKS format):
62
+ Private: .keys/mcp-private.json
63
+ Public: .keys/mcp-public.json
64
+ ```
65
+
66
+ Add `.keys/` to your `.gitignore` immediately.
67
+
68
+ ### 2. Set the environment variable
69
+
70
+ Create a `.env` file (or export directly):
71
+
72
+ ```bash
73
+ MCP_KEY_FILE_PATH=.keys/mcp-private.json
74
+ ```
75
+
76
+ ### 3. Add the middleware to your MCP server
77
+
78
+ Works with any Starlette-based MCP server:
79
+
80
+ ```python
81
+ from fastmcp import FastMCP
82
+ from mcp_auth_middleware import JWKSAuthMiddleware, get_user
83
+ import uvicorn
84
+
85
+ mcp = FastMCP("My Server")
86
+
87
+ # Register your tools on `mcp` as usual …
88
+
89
+ app = mcp.http_app()
90
+ app.add_middleware(JWKSAuthMiddleware)
91
+
92
+ if __name__ == "__main__":
93
+ uvicorn.run(app, host="0.0.0.0", port=8000)
94
+ ```
95
+
96
+ Or with the official MCP Python SDK directly:
97
+
98
+ ```python
99
+ from mcp.server.fastmcp import FastMCP
100
+ from mcp_auth_middleware import JWKSAuthMiddleware, get_user
101
+ import uvicorn
102
+
103
+ mcp = FastMCP("My Server")
104
+
105
+ app = mcp.http_app()
106
+ app.add_middleware(JWKSAuthMiddleware)
107
+
108
+ if __name__ == "__main__":
109
+ uvicorn.run(app, host="0.0.0.0", port=8000)
110
+ ```
111
+
112
+ ### 4. Access user claims inside any tool
113
+
114
+ ```python
115
+ @mcp.tool()
116
+ def whoami() -> str:
117
+ user = get_user()
118
+ return f"Hello, {user.name or 'anonymous'}!"
119
+ ```
120
+
121
+ `get_user()` returns an `AuthUser` dict. Access claims as attributes — missing keys return `None` instead of raising.
122
+
123
+ ---
124
+
125
+ ## Kubernetes deployment
126
+
127
+ ### 1. Generate keys and pipe straight into kubectl
128
+
129
+ ```bash
130
+ mcp-auth-middleware k8s | kubectl apply -f -
131
+ ```
132
+
133
+ This creates a Kubernetes `Secret` named `mcp-server-keys` in the `default` namespace. Customise with flags:
134
+
135
+ ```bash
136
+ mcp-auth-middleware k8s --namespace my-ns --secret-name my-mcp-keys | kubectl apply -f -
137
+ ```
138
+
139
+ ### 2. Clean up local key material
140
+
141
+ ```bash
142
+ mcp-auth-middleware clean
143
+ ```
144
+
145
+ Uses `shred` (Linux) for secure deletion when available; falls back to overwrite-then-delete.
146
+
147
+ ### 3. Mount the secret in your deployment
148
+
149
+ Add the following snippets to your existing Deployment YAML.
150
+
151
+ **Volume definition** (under `spec.template.spec.volumes`):
152
+
153
+ ```yaml
154
+ - name: mcp-secret-volume
155
+ secret:
156
+ secretName: mcp-server-keys # must match --secret-name
157
+ defaultMode: 0400
158
+ items:
159
+ - key: mcp_jwks
160
+ path: key.json
161
+ ```
162
+
163
+ **Volume mount** (under your container's `volumeMounts`):
164
+
165
+ ```yaml
166
+ - mountPath: /etc/mcp/secrets
167
+ name: mcp-secret-volume
168
+ readOnly: true
169
+ ```
170
+
171
+ **Environment variable** (under `env`):
172
+
173
+ ```yaml
174
+ - name: MCP_KEY_FILE_PATH
175
+ value: "/etc/mcp/secrets/key.json"
176
+ ```
177
+
178
+ A full example Deployment + Service is in [`examples/k8s-deployment.yaml`](examples/k8s-deployment.yaml).
179
+
180
+ ---
181
+
182
+ ## CLI reference
183
+
184
+ All commands accept `-o / --output <dir>` to change the key directory (default: `.keys`).
185
+
186
+ | Command | Description |
187
+ |---|---|
188
+ | `mcp-auth-middleware generate` | Generate an RSA-4096 JWKS key pair |
189
+ | `mcp-auth-middleware k8s` | Generate keys and print a Kubernetes Secret YAML to stdout |
190
+ | `mcp-auth-middleware clean` | Securely delete keys from the output directory |
191
+
192
+ ### `mcp-auth-middleware k8s` flags
193
+
194
+ | Flag | Default | Description |
195
+ |---|---|---|
196
+ | `-n, --namespace` | `default` | Kubernetes namespace |
197
+ | `-s, --secret-name` | `mcp-server-keys` | Name of the K8s Secret |
198
+
199
+ ---
200
+
201
+ ## API reference
202
+
203
+ ### `JWKSAuthMiddleware`
204
+
205
+ Starlette middleware. Attach it to any HTTP-based MCP server app:
206
+
207
+ ```python
208
+ app.add_middleware(
209
+ JWKSAuthMiddleware,
210
+ verifier=None, # optional: provide your own JWETokenVerifier
211
+ jwks_path="/.well-known/jwks.json", # public-key endpoint path
212
+ )
213
+ ```
214
+
215
+ The middleware automatically serves your server's **public** JWKS at the configured path so that token producers can discover your encryption key.
216
+
217
+ ### `get_user() -> AuthUser`
218
+
219
+ Returns the authenticated user's claims for the current request. Safe to call from any async context (tools, routes, dependencies) during request handling.
220
+
221
+ ```python
222
+ user = get_user()
223
+ user.email # claim value or None
224
+ user["role"] # also works as a regular dict
225
+ ```
226
+
227
+ ### `JWETokenVerifier`
228
+
229
+ Lower-level class if you need to verify tokens outside the middleware:
230
+
231
+ ```python
232
+ from mcp_auth_middleware import JWETokenVerifier
233
+
234
+ verifier = JWETokenVerifier() # reads MCP_KEY_FILE_PATH
235
+ claims = await verifier.verify_token(token_string)
236
+ public_jwks = verifier.get_jwks() # for publishing
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Environment variables
242
+
243
+ | Variable | Required | Description |
244
+ |---|---|---|
245
+ | `MCP_KEY_FILE_PATH` | Yes | Path to the private JWKS JSON file |
246
+
247
+ ---
248
+
249
+ ## How the token flow works
250
+
251
+ ```
252
+ Producer (client/gateway) MCP Server
253
+ ―――――――――――――――――――――――――― ――――――――――
254
+ GET /.well-known/jwks.json
255
+ ◄──── { public RSA key }
256
+ Encrypt claims with public key
257
+ POST /mcp/tool ──────────────────────►
258
+ Authorization: Bearer <JWE>
259
+ Middleware decrypts JWE
260
+ with private key
261
+ ─► get_user() returns claims
262
+ ```
263
+
264
+ 1. The token producer fetches the server's public key from `/.well-known/jwks.json`.
265
+ 2. It encrypts a JSON payload (user claims) into a JWE token using that public key.
266
+ 3. The token is sent as a `Bearer` token in the `Authorization` header.
267
+ 4. The middleware decrypts it with the private key and exposes claims via `get_user()`.
268
+
269
+ ---
270
+
271
+ ## Project structure
272
+
273
+ ```
274
+ mcp-auth-middleware/
275
+ ├── pyproject.toml
276
+ ├── README.md
277
+ ├── LICENSE
278
+ ├── CHANGELOG.md
279
+ ├── requirements.txt
280
+ ├── requirements-dev.txt
281
+ ├── MANIFEST.in
282
+ ├── .gitignore
283
+ ├── mcp_auth_middleware/
284
+ │ ├── __init__.py
285
+ │ ├── py.typed
286
+ │ ├── cli.py
287
+ │ ├── middleware.py
288
+ │ └── verifier.py
289
+ ├── tests/
290
+ └── examples/
291
+ ├── server.py
292
+ └── k8s-deployment.yaml
293
+ ```
294
+
295
+ ---
296
+ ## License
297
+
298
+ MIT
@@ -0,0 +1,272 @@
1
+ # mcp-auth-middleware
2
+
3
+ JWE authentication middleware for HTTP MCP servers. Encrypt user data end-to-end so that only your MCP server can read it.
4
+
5
+ ## What it does
6
+
7
+ `mcp-auth-middleware` gives your MCP server two things:
8
+
9
+ 1. **A middleware** that intercepts incoming requests, decrypts a JWE Bearer token, and makes the authenticated user's claims available via `get_user()`.
10
+ 2. **A CLI** (`mcp-auth-middleware`) that generates RSA key pairs in JWKS format, outputs Kubernetes Secret YAML, and securely deletes local keys when you're done.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install mcp-auth-middleware
18
+ ```
19
+
20
+ This installs the library **and** the `mcp-auth-middleware` CLI.
21
+
22
+ ---
23
+
24
+ ## Quick start (local development)
25
+
26
+ ### 1. Generate keys
27
+
28
+ ```bash
29
+ mcp-auth-middleware generate
30
+ ```
31
+
32
+ Output:
33
+
34
+ ```
35
+ Keys generated (JWKS format):
36
+ Private: .keys/mcp-private.json
37
+ Public: .keys/mcp-public.json
38
+ ```
39
+
40
+ Add `.keys/` to your `.gitignore` immediately.
41
+
42
+ ### 2. Set the environment variable
43
+
44
+ Create a `.env` file (or export directly):
45
+
46
+ ```bash
47
+ MCP_KEY_FILE_PATH=.keys/mcp-private.json
48
+ ```
49
+
50
+ ### 3. Add the middleware to your MCP server
51
+
52
+ Works with any Starlette-based MCP server:
53
+
54
+ ```python
55
+ from fastmcp import FastMCP
56
+ from mcp_auth_middleware import JWKSAuthMiddleware, get_user
57
+ import uvicorn
58
+
59
+ mcp = FastMCP("My Server")
60
+
61
+ # Register your tools on `mcp` as usual …
62
+
63
+ app = mcp.http_app()
64
+ app.add_middleware(JWKSAuthMiddleware)
65
+
66
+ if __name__ == "__main__":
67
+ uvicorn.run(app, host="0.0.0.0", port=8000)
68
+ ```
69
+
70
+ Or with the official MCP Python SDK directly:
71
+
72
+ ```python
73
+ from mcp.server.fastmcp import FastMCP
74
+ from mcp_auth_middleware import JWKSAuthMiddleware, get_user
75
+ import uvicorn
76
+
77
+ mcp = FastMCP("My Server")
78
+
79
+ app = mcp.http_app()
80
+ app.add_middleware(JWKSAuthMiddleware)
81
+
82
+ if __name__ == "__main__":
83
+ uvicorn.run(app, host="0.0.0.0", port=8000)
84
+ ```
85
+
86
+ ### 4. Access user claims inside any tool
87
+
88
+ ```python
89
+ @mcp.tool()
90
+ def whoami() -> str:
91
+ user = get_user()
92
+ return f"Hello, {user.name or 'anonymous'}!"
93
+ ```
94
+
95
+ `get_user()` returns an `AuthUser` dict. Access claims as attributes — missing keys return `None` instead of raising.
96
+
97
+ ---
98
+
99
+ ## Kubernetes deployment
100
+
101
+ ### 1. Generate keys and pipe straight into kubectl
102
+
103
+ ```bash
104
+ mcp-auth-middleware k8s | kubectl apply -f -
105
+ ```
106
+
107
+ This creates a Kubernetes `Secret` named `mcp-server-keys` in the `default` namespace. Customise with flags:
108
+
109
+ ```bash
110
+ mcp-auth-middleware k8s --namespace my-ns --secret-name my-mcp-keys | kubectl apply -f -
111
+ ```
112
+
113
+ ### 2. Clean up local key material
114
+
115
+ ```bash
116
+ mcp-auth-middleware clean
117
+ ```
118
+
119
+ Uses `shred` (Linux) for secure deletion when available; falls back to overwrite-then-delete.
120
+
121
+ ### 3. Mount the secret in your deployment
122
+
123
+ Add the following snippets to your existing Deployment YAML.
124
+
125
+ **Volume definition** (under `spec.template.spec.volumes`):
126
+
127
+ ```yaml
128
+ - name: mcp-secret-volume
129
+ secret:
130
+ secretName: mcp-server-keys # must match --secret-name
131
+ defaultMode: 0400
132
+ items:
133
+ - key: mcp_jwks
134
+ path: key.json
135
+ ```
136
+
137
+ **Volume mount** (under your container's `volumeMounts`):
138
+
139
+ ```yaml
140
+ - mountPath: /etc/mcp/secrets
141
+ name: mcp-secret-volume
142
+ readOnly: true
143
+ ```
144
+
145
+ **Environment variable** (under `env`):
146
+
147
+ ```yaml
148
+ - name: MCP_KEY_FILE_PATH
149
+ value: "/etc/mcp/secrets/key.json"
150
+ ```
151
+
152
+ A full example Deployment + Service is in [`examples/k8s-deployment.yaml`](examples/k8s-deployment.yaml).
153
+
154
+ ---
155
+
156
+ ## CLI reference
157
+
158
+ All commands accept `-o / --output <dir>` to change the key directory (default: `.keys`).
159
+
160
+ | Command | Description |
161
+ |---|---|
162
+ | `mcp-auth-middleware generate` | Generate an RSA-4096 JWKS key pair |
163
+ | `mcp-auth-middleware k8s` | Generate keys and print a Kubernetes Secret YAML to stdout |
164
+ | `mcp-auth-middleware clean` | Securely delete keys from the output directory |
165
+
166
+ ### `mcp-auth-middleware k8s` flags
167
+
168
+ | Flag | Default | Description |
169
+ |---|---|---|
170
+ | `-n, --namespace` | `default` | Kubernetes namespace |
171
+ | `-s, --secret-name` | `mcp-server-keys` | Name of the K8s Secret |
172
+
173
+ ---
174
+
175
+ ## API reference
176
+
177
+ ### `JWKSAuthMiddleware`
178
+
179
+ Starlette middleware. Attach it to any HTTP-based MCP server app:
180
+
181
+ ```python
182
+ app.add_middleware(
183
+ JWKSAuthMiddleware,
184
+ verifier=None, # optional: provide your own JWETokenVerifier
185
+ jwks_path="/.well-known/jwks.json", # public-key endpoint path
186
+ )
187
+ ```
188
+
189
+ The middleware automatically serves your server's **public** JWKS at the configured path so that token producers can discover your encryption key.
190
+
191
+ ### `get_user() -> AuthUser`
192
+
193
+ Returns the authenticated user's claims for the current request. Safe to call from any async context (tools, routes, dependencies) during request handling.
194
+
195
+ ```python
196
+ user = get_user()
197
+ user.email # claim value or None
198
+ user["role"] # also works as a regular dict
199
+ ```
200
+
201
+ ### `JWETokenVerifier`
202
+
203
+ Lower-level class if you need to verify tokens outside the middleware:
204
+
205
+ ```python
206
+ from mcp_auth_middleware import JWETokenVerifier
207
+
208
+ verifier = JWETokenVerifier() # reads MCP_KEY_FILE_PATH
209
+ claims = await verifier.verify_token(token_string)
210
+ public_jwks = verifier.get_jwks() # for publishing
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Environment variables
216
+
217
+ | Variable | Required | Description |
218
+ |---|---|---|
219
+ | `MCP_KEY_FILE_PATH` | Yes | Path to the private JWKS JSON file |
220
+
221
+ ---
222
+
223
+ ## How the token flow works
224
+
225
+ ```
226
+ Producer (client/gateway) MCP Server
227
+ ―――――――――――――――――――――――――― ――――――――――
228
+ GET /.well-known/jwks.json
229
+ ◄──── { public RSA key }
230
+ Encrypt claims with public key
231
+ POST /mcp/tool ──────────────────────►
232
+ Authorization: Bearer <JWE>
233
+ Middleware decrypts JWE
234
+ with private key
235
+ ─► get_user() returns claims
236
+ ```
237
+
238
+ 1. The token producer fetches the server's public key from `/.well-known/jwks.json`.
239
+ 2. It encrypts a JSON payload (user claims) into a JWE token using that public key.
240
+ 3. The token is sent as a `Bearer` token in the `Authorization` header.
241
+ 4. The middleware decrypts it with the private key and exposes claims via `get_user()`.
242
+
243
+ ---
244
+
245
+ ## Project structure
246
+
247
+ ```
248
+ mcp-auth-middleware/
249
+ ├── pyproject.toml
250
+ ├── README.md
251
+ ├── LICENSE
252
+ ├── CHANGELOG.md
253
+ ├── requirements.txt
254
+ ├── requirements-dev.txt
255
+ ├── MANIFEST.in
256
+ ├── .gitignore
257
+ ├── mcp_auth_middleware/
258
+ │ ├── __init__.py
259
+ │ ├── py.typed
260
+ │ ├── cli.py
261
+ │ ├── middleware.py
262
+ │ └── verifier.py
263
+ ├── tests/
264
+ └── examples/
265
+ ├── server.py
266
+ └── k8s-deployment.yaml
267
+ ```
268
+
269
+ ---
270
+ ## License
271
+
272
+ MIT
@@ -0,0 +1,7 @@
1
+ """MCP Secrets - JWE authentication middleware for FastMCP/Starlette."""
2
+
3
+ from .middleware import AuthUser, JWKSAuthMiddleware, get_user
4
+ from .verifier import JWETokenVerifier
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["JWETokenVerifier", "JWKSAuthMiddleware", "get_user", "AuthUser"]
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env python3
2
+ """MCP Secrets CLI - Key management for JWE authentication."""
3
+
4
+ import argparse
5
+ import base64
6
+ import json
7
+ import os
8
+ import secrets
9
+ import shutil
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ try:
15
+ from jwcrypto import jwk
16
+ except ImportError:
17
+ print("Error: 'jwcrypto' library is required.", file=sys.stderr)
18
+ print("Please run: pip install jwcrypto", file=sys.stderr)
19
+ sys.exit(1)
20
+
21
+ KEY_SIZE = 4096
22
+ KEY_FILES = ("mcp-private.json", "mcp-public.json")
23
+
24
+
25
+ def generate_keys(output_dir: Path) -> tuple[Path, Path]:
26
+ """Generate an RSA JWKS key pair and write to output_dir."""
27
+ output_dir.mkdir(parents=True, exist_ok=True)
28
+
29
+ key = jwk.JWK.generate(kty="RSA", size=KEY_SIZE)
30
+ key["kid"] = key.thumbprint()
31
+
32
+ private_jwks = {"keys": [json.loads(key.export_private())]}
33
+ public_jwks = {"keys": [json.loads(key.export_public())]}
34
+
35
+ private_key_path = output_dir / "mcp-private.json"
36
+ public_key_path = output_dir / "mcp-public.json"
37
+
38
+ with open(private_key_path, "w") as f:
39
+ json.dump(private_jwks, f, indent=2)
40
+
41
+ try:
42
+ os.chmod(private_key_path, 0o600)
43
+ except OSError:
44
+ pass
45
+
46
+ with open(public_key_path, "w") as f:
47
+ json.dump(public_jwks, f, indent=2)
48
+
49
+ return private_key_path, public_key_path
50
+
51
+
52
+ def secure_delete(file_path: Path) -> None:
53
+ """Securely delete a file using shred or overwrite fallback."""
54
+ if not file_path.exists():
55
+ return
56
+
57
+ if shutil.which("shred"):
58
+ subprocess.run(
59
+ ["shred", "--remove", "--zero", "--iterations=3", str(file_path)],
60
+ check=True,
61
+ )
62
+ return
63
+
64
+ try:
65
+ size = file_path.stat().st_size
66
+ with open(file_path, "r+b") as f:
67
+ f.write(secrets.token_bytes(size))
68
+ f.flush()
69
+ os.fsync(f.fileno())
70
+ except OSError:
71
+ pass
72
+
73
+ file_path.unlink()
74
+
75
+
76
+ def clean_keys(key_dir: Path) -> list[str]:
77
+ """Remove key files from key_dir. Returns list of removed paths."""
78
+ removed = []
79
+ for name in KEY_FILES:
80
+ key_file = key_dir / name
81
+ if not key_file.exists():
82
+ continue
83
+
84
+ if "private" in name:
85
+ secure_delete(key_file)
86
+ else:
87
+ key_file.unlink()
88
+
89
+ removed.append(str(key_file))
90
+ return removed
91
+
92
+
93
+ def cmd_generate(args: argparse.Namespace) -> None:
94
+ output_dir = args.output.resolve()
95
+ private_key, public_key = generate_keys(output_dir)
96
+
97
+ print(f"Keys generated (JWKS format):")
98
+ print(f" Private: {private_key}")
99
+ print(f" Public: {public_key}")
100
+ print(f"\nFor local development, add to .env:")
101
+ print(f" MCP_KEY_FILE_PATH={private_key}")
102
+ print(f"\nAdd to .gitignore:")
103
+ print(f" .keys/")
104
+
105
+
106
+ def cmd_k8s(args: argparse.Namespace) -> None:
107
+ output_dir = args.output.resolve()
108
+ private_key, _ = generate_keys(output_dir)
109
+
110
+ with open(private_key, "rb") as f:
111
+ b64_key = base64.b64encode(f.read()).decode("utf-8")
112
+
113
+ k8s_yaml = f"""\
114
+ apiVersion: v1
115
+ kind: Secret
116
+ metadata:
117
+ name: {args.secret_name}
118
+ namespace: {args.namespace}
119
+ type: Opaque
120
+ data:
121
+ mcp_jwks: {b64_key}
122
+ """
123
+ print(k8s_yaml)
124
+ print(f"# Apply: mcp-secrets k8s | kubectl apply -f -", file=sys.stderr)
125
+ print(f"# Then clean local keys: mcp-secrets clean", file=sys.stderr)
126
+
127
+
128
+ def cmd_clean(args: argparse.Namespace) -> None:
129
+ removed = clean_keys(args.output.resolve())
130
+ if removed:
131
+ print("Removed:\n " + "\n ".join(removed))
132
+ else:
133
+ print(f"No keys found in {args.output}")
134
+
135
+
136
+ def main() -> None:
137
+ parser = argparse.ArgumentParser(
138
+ prog="mcp-secrets", description="MCP Secrets - Key Management (JWKS)"
139
+ )
140
+ subs = parser.add_subparsers(dest="command", required=True)
141
+
142
+ commands = [
143
+ ("generate", "Generate RSA JWKS key pair", cmd_generate),
144
+ ("k8s", "Generate keys and output Kubernetes secret YAML", cmd_k8s),
145
+ ("clean", "Remove keys securely", cmd_clean),
146
+ ]
147
+
148
+ for name, help_text, handler in commands:
149
+ p = subs.add_parser(name, help=help_text)
150
+ p.add_argument(
151
+ "-o",
152
+ "--output",
153
+ type=Path,
154
+ default=Path(".keys"),
155
+ help="Key directory (default: .keys)",
156
+ )
157
+ p.set_defaults(func=handler)
158
+
159
+ k8s = subs.choices["k8s"]
160
+ k8s.add_argument(
161
+ "-n", "--namespace", default="default", help="Kubernetes namespace"
162
+ )
163
+ k8s.add_argument(
164
+ "-s", "--secret-name", default="mcp-server-keys", help="Secret name"
165
+ )
166
+
167
+ args = parser.parse_args()
168
+ args.func(args)
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
@@ -0,0 +1,61 @@
1
+ """JWE authentication middleware for Starlette/FastMCP."""
2
+
3
+ from contextvars import ContextVar
4
+ from typing import Any
5
+
6
+ from starlette.middleware.base import BaseHTTPMiddleware
7
+ from starlette.requests import Request
8
+ from starlette.responses import JSONResponse, Response
9
+
10
+ from .verifier import JWETokenVerifier
11
+
12
+ _user_context: ContextVar[dict] = ContextVar("user", default={})
13
+
14
+
15
+ class AuthUser(dict):
16
+ """User claims with attribute access. Missing keys return None."""
17
+
18
+ def __getattr__(self, name: str) -> Any:
19
+ return self.get(name)
20
+
21
+
22
+ def get_user() -> AuthUser:
23
+ """Get authenticated user from current request context."""
24
+ return AuthUser(_user_context.get())
25
+
26
+
27
+ class JWKSAuthMiddleware(BaseHTTPMiddleware):
28
+ """
29
+ Middleware that decrypts JWE Bearer tokens and serves JWKS endpoint.
30
+
31
+ Usage:
32
+ app = mcp.http_app()
33
+ app.add_middleware(JWKSAuthMiddleware)
34
+ uvicorn.run(app, host="0.0.0.0", port=8000)
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ app,
40
+ verifier: JWETokenVerifier | None = None,
41
+ jwks_path: str = "/.well-known/jwks.json",
42
+ ):
43
+ super().__init__(app)
44
+ self.verifier = verifier or JWETokenVerifier()
45
+ self.jwks_path = jwks_path
46
+
47
+ async def dispatch(self, request: Request, call_next) -> Response:
48
+ if request.url.path == self.jwks_path:
49
+ return JSONResponse(self.verifier.get_jwks())
50
+
51
+ claims = {}
52
+ if (auth := request.headers.get("authorization", "")).lower().startswith(
53
+ "bearer "
54
+ ):
55
+ claims = await self.verifier.verify_token(auth[7:]) or {}
56
+
57
+ token = _user_context.set(claims)
58
+ try:
59
+ return await call_next(request)
60
+ finally:
61
+ _user_context.reset(token)
File without changes
@@ -0,0 +1,52 @@
1
+ """JWE token verification and JWKS support."""
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from typing import Any
7
+
8
+ from jose import jwe
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class JWETokenVerifier:
14
+ """Verifies JWE tokens and exposes public key as JWKS."""
15
+
16
+ def __init__(self) -> None:
17
+ self._jwk = self._load_key_from_env()
18
+
19
+ @staticmethod
20
+ def _load_key_from_env() -> dict | None:
21
+ path = os.environ.get("MCP_KEY_FILE_PATH")
22
+ if not path:
23
+ return None
24
+
25
+ try:
26
+ with open(path) as f:
27
+ data = json.load(f)
28
+
29
+ if "keys" in data and isinstance(data["keys"], list):
30
+ return data["keys"][0]
31
+ return data
32
+
33
+ except (FileNotFoundError, json.JSONDecodeError) as e:
34
+ logger.error(f"Key loading error: {e}")
35
+ return None
36
+
37
+ async def verify_token(self, token: str) -> dict[str, Any] | None:
38
+ if not self._jwk:
39
+ return None
40
+ try:
41
+ payload = json.loads(jwe.decrypt(token, self._jwk))
42
+ return payload.get("data", payload)
43
+ except Exception as e:
44
+ logger.debug(f"Token verification failed: {e}")
45
+ return None
46
+
47
+ def get_jwks(self) -> dict[str, Any]:
48
+ if not self._jwk:
49
+ return {"keys": []}
50
+ public_fields = {"kty", "kid", "alg", "use", "n", "e"}
51
+ public_jwk = {k: v for k, v in self._jwk.items() if k in public_fields}
52
+ return {"keys": [public_jwk]}
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-auth-middleware
3
+ Version: 0.1.0
4
+ Summary: JWE authentication middleware for MCP/Starlette applications
5
+ Author: Aurel Bartmann
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/fhswf/mcp-auth-middleware
8
+ Project-URL: Documentation, https://github.com/fhswf/mcp-auth-middleware#readme
9
+ Project-URL: Repository, https://github.com/fhswf/mcp-auth-middleware
10
+ Project-URL: Issues, https://github.com/fhswf/mcp-auth-middleware/issues
11
+ Keywords: mcp,jwe,jwks,authentication,middleware,starlette
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Framework :: FastAPI
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: Security :: Cryptography
19
+ Requires-Python: >=3.13
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: python-jose[cryptography]>=3.5.0
23
+ Requires-Dist: starlette>=0.52.1
24
+ Requires-Dist: jwcrypto>=1.5.6
25
+ Dynamic: license-file
26
+
27
+ # mcp-auth-middleware
28
+
29
+ JWE authentication middleware for HTTP MCP servers. Encrypt user data end-to-end so that only your MCP server can read it.
30
+
31
+ ## What it does
32
+
33
+ `mcp-auth-middleware` gives your MCP server two things:
34
+
35
+ 1. **A middleware** that intercepts incoming requests, decrypts a JWE Bearer token, and makes the authenticated user's claims available via `get_user()`.
36
+ 2. **A CLI** (`mcp-auth-middleware`) that generates RSA key pairs in JWKS format, outputs Kubernetes Secret YAML, and securely deletes local keys when you're done.
37
+
38
+ ---
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install mcp-auth-middleware
44
+ ```
45
+
46
+ This installs the library **and** the `mcp-auth-middleware` CLI.
47
+
48
+ ---
49
+
50
+ ## Quick start (local development)
51
+
52
+ ### 1. Generate keys
53
+
54
+ ```bash
55
+ mcp-auth-middleware generate
56
+ ```
57
+
58
+ Output:
59
+
60
+ ```
61
+ Keys generated (JWKS format):
62
+ Private: .keys/mcp-private.json
63
+ Public: .keys/mcp-public.json
64
+ ```
65
+
66
+ Add `.keys/` to your `.gitignore` immediately.
67
+
68
+ ### 2. Set the environment variable
69
+
70
+ Create a `.env` file (or export directly):
71
+
72
+ ```bash
73
+ MCP_KEY_FILE_PATH=.keys/mcp-private.json
74
+ ```
75
+
76
+ ### 3. Add the middleware to your MCP server
77
+
78
+ Works with any Starlette-based MCP server:
79
+
80
+ ```python
81
+ from fastmcp import FastMCP
82
+ from mcp_auth_middleware import JWKSAuthMiddleware, get_user
83
+ import uvicorn
84
+
85
+ mcp = FastMCP("My Server")
86
+
87
+ # Register your tools on `mcp` as usual …
88
+
89
+ app = mcp.http_app()
90
+ app.add_middleware(JWKSAuthMiddleware)
91
+
92
+ if __name__ == "__main__":
93
+ uvicorn.run(app, host="0.0.0.0", port=8000)
94
+ ```
95
+
96
+ Or with the official MCP Python SDK directly:
97
+
98
+ ```python
99
+ from mcp.server.fastmcp import FastMCP
100
+ from mcp_auth_middleware import JWKSAuthMiddleware, get_user
101
+ import uvicorn
102
+
103
+ mcp = FastMCP("My Server")
104
+
105
+ app = mcp.http_app()
106
+ app.add_middleware(JWKSAuthMiddleware)
107
+
108
+ if __name__ == "__main__":
109
+ uvicorn.run(app, host="0.0.0.0", port=8000)
110
+ ```
111
+
112
+ ### 4. Access user claims inside any tool
113
+
114
+ ```python
115
+ @mcp.tool()
116
+ def whoami() -> str:
117
+ user = get_user()
118
+ return f"Hello, {user.name or 'anonymous'}!"
119
+ ```
120
+
121
+ `get_user()` returns an `AuthUser` dict. Access claims as attributes — missing keys return `None` instead of raising.
122
+
123
+ ---
124
+
125
+ ## Kubernetes deployment
126
+
127
+ ### 1. Generate keys and pipe straight into kubectl
128
+
129
+ ```bash
130
+ mcp-auth-middleware k8s | kubectl apply -f -
131
+ ```
132
+
133
+ This creates a Kubernetes `Secret` named `mcp-server-keys` in the `default` namespace. Customise with flags:
134
+
135
+ ```bash
136
+ mcp-auth-middleware k8s --namespace my-ns --secret-name my-mcp-keys | kubectl apply -f -
137
+ ```
138
+
139
+ ### 2. Clean up local key material
140
+
141
+ ```bash
142
+ mcp-auth-middleware clean
143
+ ```
144
+
145
+ Uses `shred` (Linux) for secure deletion when available; falls back to overwrite-then-delete.
146
+
147
+ ### 3. Mount the secret in your deployment
148
+
149
+ Add the following snippets to your existing Deployment YAML.
150
+
151
+ **Volume definition** (under `spec.template.spec.volumes`):
152
+
153
+ ```yaml
154
+ - name: mcp-secret-volume
155
+ secret:
156
+ secretName: mcp-server-keys # must match --secret-name
157
+ defaultMode: 0400
158
+ items:
159
+ - key: mcp_jwks
160
+ path: key.json
161
+ ```
162
+
163
+ **Volume mount** (under your container's `volumeMounts`):
164
+
165
+ ```yaml
166
+ - mountPath: /etc/mcp/secrets
167
+ name: mcp-secret-volume
168
+ readOnly: true
169
+ ```
170
+
171
+ **Environment variable** (under `env`):
172
+
173
+ ```yaml
174
+ - name: MCP_KEY_FILE_PATH
175
+ value: "/etc/mcp/secrets/key.json"
176
+ ```
177
+
178
+ A full example Deployment + Service is in [`examples/k8s-deployment.yaml`](examples/k8s-deployment.yaml).
179
+
180
+ ---
181
+
182
+ ## CLI reference
183
+
184
+ All commands accept `-o / --output <dir>` to change the key directory (default: `.keys`).
185
+
186
+ | Command | Description |
187
+ |---|---|
188
+ | `mcp-auth-middleware generate` | Generate an RSA-4096 JWKS key pair |
189
+ | `mcp-auth-middleware k8s` | Generate keys and print a Kubernetes Secret YAML to stdout |
190
+ | `mcp-auth-middleware clean` | Securely delete keys from the output directory |
191
+
192
+ ### `mcp-auth-middleware k8s` flags
193
+
194
+ | Flag | Default | Description |
195
+ |---|---|---|
196
+ | `-n, --namespace` | `default` | Kubernetes namespace |
197
+ | `-s, --secret-name` | `mcp-server-keys` | Name of the K8s Secret |
198
+
199
+ ---
200
+
201
+ ## API reference
202
+
203
+ ### `JWKSAuthMiddleware`
204
+
205
+ Starlette middleware. Attach it to any HTTP-based MCP server app:
206
+
207
+ ```python
208
+ app.add_middleware(
209
+ JWKSAuthMiddleware,
210
+ verifier=None, # optional: provide your own JWETokenVerifier
211
+ jwks_path="/.well-known/jwks.json", # public-key endpoint path
212
+ )
213
+ ```
214
+
215
+ The middleware automatically serves your server's **public** JWKS at the configured path so that token producers can discover your encryption key.
216
+
217
+ ### `get_user() -> AuthUser`
218
+
219
+ Returns the authenticated user's claims for the current request. Safe to call from any async context (tools, routes, dependencies) during request handling.
220
+
221
+ ```python
222
+ user = get_user()
223
+ user.email # claim value or None
224
+ user["role"] # also works as a regular dict
225
+ ```
226
+
227
+ ### `JWETokenVerifier`
228
+
229
+ Lower-level class if you need to verify tokens outside the middleware:
230
+
231
+ ```python
232
+ from mcp_auth_middleware import JWETokenVerifier
233
+
234
+ verifier = JWETokenVerifier() # reads MCP_KEY_FILE_PATH
235
+ claims = await verifier.verify_token(token_string)
236
+ public_jwks = verifier.get_jwks() # for publishing
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Environment variables
242
+
243
+ | Variable | Required | Description |
244
+ |---|---|---|
245
+ | `MCP_KEY_FILE_PATH` | Yes | Path to the private JWKS JSON file |
246
+
247
+ ---
248
+
249
+ ## How the token flow works
250
+
251
+ ```
252
+ Producer (client/gateway) MCP Server
253
+ ―――――――――――――――――――――――――― ――――――――――
254
+ GET /.well-known/jwks.json
255
+ ◄──── { public RSA key }
256
+ Encrypt claims with public key
257
+ POST /mcp/tool ──────────────────────►
258
+ Authorization: Bearer <JWE>
259
+ Middleware decrypts JWE
260
+ with private key
261
+ ─► get_user() returns claims
262
+ ```
263
+
264
+ 1. The token producer fetches the server's public key from `/.well-known/jwks.json`.
265
+ 2. It encrypts a JSON payload (user claims) into a JWE token using that public key.
266
+ 3. The token is sent as a `Bearer` token in the `Authorization` header.
267
+ 4. The middleware decrypts it with the private key and exposes claims via `get_user()`.
268
+
269
+ ---
270
+
271
+ ## Project structure
272
+
273
+ ```
274
+ mcp-auth-middleware/
275
+ ├── pyproject.toml
276
+ ├── README.md
277
+ ├── LICENSE
278
+ ├── CHANGELOG.md
279
+ ├── requirements.txt
280
+ ├── requirements-dev.txt
281
+ ├── MANIFEST.in
282
+ ├── .gitignore
283
+ ├── mcp_auth_middleware/
284
+ │ ├── __init__.py
285
+ │ ├── py.typed
286
+ │ ├── cli.py
287
+ │ ├── middleware.py
288
+ │ └── verifier.py
289
+ ├── tests/
290
+ └── examples/
291
+ ├── server.py
292
+ └── k8s-deployment.yaml
293
+ ```
294
+
295
+ ---
296
+ ## License
297
+
298
+ MIT
@@ -0,0 +1,17 @@
1
+ CHANGELOG.md
2
+ LICENSE
3
+ MANIFEST.in
4
+ README.md
5
+ pyproject.toml
6
+ requirements.txt
7
+ mcp_auth_middleware/__init__.py
8
+ mcp_auth_middleware/cli.py
9
+ mcp_auth_middleware/middleware.py
10
+ mcp_auth_middleware/py.typed
11
+ mcp_auth_middleware/verifier.py
12
+ mcp_auth_middleware.egg-info/PKG-INFO
13
+ mcp_auth_middleware.egg-info/SOURCES.txt
14
+ mcp_auth_middleware.egg-info/dependency_links.txt
15
+ mcp_auth_middleware.egg-info/entry_points.txt
16
+ mcp_auth_middleware.egg-info/requires.txt
17
+ mcp_auth_middleware.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-auth-middleware = mcp_auth_middleware.cli:main
@@ -0,0 +1,3 @@
1
+ python-jose[cryptography]>=3.5.0
2
+ starlette>=0.52.1
3
+ jwcrypto>=1.5.6
@@ -0,0 +1 @@
1
+ mcp_auth_middleware
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mcp-auth-middleware"
7
+ version = "0.1.0"
8
+ description = "JWE authentication middleware for MCP/Starlette applications"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.13"
12
+ authors = [
13
+ { name = "Aurel Bartmann" },
14
+ ]
15
+ keywords = ["mcp", "jwe", "jwks", "authentication", "middleware", "starlette"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Framework :: FastAPI",
22
+ "Topic :: Security",
23
+ "Topic :: Security :: Cryptography",
24
+ ]
25
+ dependencies = [
26
+ "python-jose[cryptography]>=3.5.0",
27
+ "starlette>=0.52.1",
28
+ "jwcrypto>=1.5.6",
29
+ ]
30
+
31
+ [project.scripts]
32
+ mcp-auth-middleware = "mcp_auth_middleware.cli:main"
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/fhswf/mcp-auth-middleware"
36
+ Documentation = "https://github.com/fhswf/mcp-auth-middleware#readme"
37
+ Repository = "https://github.com/fhswf/mcp-auth-middleware"
38
+ Issues = "https://github.com/fhswf/mcp-auth-middleware/issues"
39
+
40
+ [tool.setuptools.packages.find]
41
+ include = ["mcp_auth_middleware*"]
@@ -0,0 +1,3 @@
1
+ python-jose[cryptography]>=3.3.0
2
+ starlette>=0.27.0
3
+ jwcrypto>=1.5.6
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+