fastmcp-auth 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.
- fastmcp_auth-0.1.0/CHANGELOG.md +12 -0
- fastmcp_auth-0.1.0/LICENSE +21 -0
- fastmcp_auth-0.1.0/MANIFEST.in +5 -0
- fastmcp_auth-0.1.0/PKG-INFO +280 -0
- fastmcp_auth-0.1.0/README.md +254 -0
- fastmcp_auth-0.1.0/fastmcp_auth/__init__.py +7 -0
- fastmcp_auth-0.1.0/fastmcp_auth/cli.py +172 -0
- fastmcp_auth-0.1.0/fastmcp_auth/middleware.py +61 -0
- fastmcp_auth-0.1.0/fastmcp_auth/verifier.py +52 -0
- fastmcp_auth-0.1.0/fastmcp_auth.egg-info/PKG-INFO +280 -0
- fastmcp_auth-0.1.0/fastmcp_auth.egg-info/SOURCES.txt +16 -0
- fastmcp_auth-0.1.0/fastmcp_auth.egg-info/dependency_links.txt +1 -0
- fastmcp_auth-0.1.0/fastmcp_auth.egg-info/entry_points.txt +2 -0
- fastmcp_auth-0.1.0/fastmcp_auth.egg-info/requires.txt +3 -0
- fastmcp_auth-0.1.0/fastmcp_auth.egg-info/top_level.txt +1 -0
- fastmcp_auth-0.1.0/pyproject.toml +41 -0
- fastmcp_auth-0.1.0/requirements.txt +3 -0
- fastmcp_auth-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- JWE token decryption middleware for Starlette/FastMCP
|
|
8
|
+
- `get_user()` context accessor for authenticated claims
|
|
9
|
+
- Public JWKS endpoint at `/.well-known/jwks.json`
|
|
10
|
+
- CLI: `fastmcp-auth generate` — RSA-4096 JWKS key pair generation
|
|
11
|
+
- CLI: `fastmcp-auth k8s` — Kubernetes Secret YAML output
|
|
12
|
+
- CLI: `fastmcp-auth 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,280 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastmcp-auth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: JWE authentication middleware for FastMCP/Starlette applications
|
|
5
|
+
Author: Aurel Bartmann
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fhswf/fastmcp-auth
|
|
8
|
+
Project-URL: Documentation, https://github.com/fhswf/fastmcp-auth#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/fhswf/fastmcp-auth
|
|
10
|
+
Project-URL: Issues, https://github.com/fhswf/fastmcp-auth/issues
|
|
11
|
+
Keywords: mcp,fastmcp,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
|
+
# fastmcp-auth
|
|
28
|
+
|
|
29
|
+
JWE authentication middleware for [FastMCP](https://github.com/jlowin/fastmcp) / Starlette servers. Encrypt user data end-to-end so that only your MCP server can read it.
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
`fastmcp-auth` gives your FastMCP 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** (`fastmcp-auth`) 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 fastmcp-auth
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This installs the library **and** the `fastmcp-auth` CLI.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick start (local development)
|
|
51
|
+
|
|
52
|
+
### 1. Generate keys
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
fastmcp-auth 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 FastMCP server
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fastmcp import FastMCP
|
|
80
|
+
from fastmcp_auth import JWKSAuthMiddleware, get_user
|
|
81
|
+
import uvicorn
|
|
82
|
+
|
|
83
|
+
mcp = FastMCP("My Server")
|
|
84
|
+
|
|
85
|
+
# Register your tools on `mcp` as usual …
|
|
86
|
+
|
|
87
|
+
app = mcp.http_app()
|
|
88
|
+
app.add_middleware(JWKSAuthMiddleware)
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. Access user claims inside any tool
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
@mcp.tool()
|
|
98
|
+
def whoami() -> str:
|
|
99
|
+
user = get_user()
|
|
100
|
+
return f"Hello, {user.name or 'anonymous'}!"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`get_user()` returns an `AuthUser` dict. Access claims as attributes — missing keys return `None` instead of raising.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Kubernetes deployment
|
|
108
|
+
|
|
109
|
+
### 1. Generate keys and pipe straight into kubectl
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
fastmcp-auth k8s | kubectl apply -f -
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This creates a Kubernetes `Secret` named `mcp-server-keys` in the `default` namespace. Customise with flags:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
fastmcp-auth k8s --namespace my-ns --secret-name my-mcp-keys | kubectl apply -f -
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Clean up local key material
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
fastmcp-auth clean
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Uses `shred` (Linux) for secure deletion when available; falls back to overwrite-then-delete.
|
|
128
|
+
|
|
129
|
+
### 3. Mount the secret in your deployment
|
|
130
|
+
|
|
131
|
+
Add the following snippets to your existing Deployment YAML.
|
|
132
|
+
|
|
133
|
+
**Volume definition** (under `spec.template.spec.volumes`):
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
- name: mcp-secret-volume
|
|
137
|
+
secret:
|
|
138
|
+
secretName: mcp-server-keys # must match --secret-name
|
|
139
|
+
defaultMode: 0400
|
|
140
|
+
items:
|
|
141
|
+
- key: mcp_jwks
|
|
142
|
+
path: key.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Volume mount** (under your container's `volumeMounts`):
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
- mountPath: /etc/mcp/secrets
|
|
149
|
+
name: mcp-secret-volume
|
|
150
|
+
readOnly: true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Environment variable** (under `env`):
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
- name: MCP_KEY_FILE_PATH
|
|
157
|
+
value: "/etc/mcp/secrets/key.json"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A full example Deployment + Service is in [`examples/k8s-deployment.yaml`](examples/k8s-deployment.yaml).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## CLI reference
|
|
165
|
+
|
|
166
|
+
All commands accept `-o / --output <dir>` to change the key directory (default: `.keys`).
|
|
167
|
+
|
|
168
|
+
| Command | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `fastmcp-auth generate` | Generate an RSA-4096 JWKS key pair |
|
|
171
|
+
| `fastmcp-auth k8s` | Generate keys and print a Kubernetes Secret YAML to stdout |
|
|
172
|
+
| `fastmcp-auth clean` | Securely delete keys from the output directory |
|
|
173
|
+
|
|
174
|
+
### `fastmcp-auth k8s` flags
|
|
175
|
+
|
|
176
|
+
| Flag | Default | Description |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `-n, --namespace` | `default` | Kubernetes namespace |
|
|
179
|
+
| `-s, --secret-name` | `mcp-server-keys` | Name of the K8s Secret |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## API reference
|
|
184
|
+
|
|
185
|
+
### `JWKSAuthMiddleware`
|
|
186
|
+
|
|
187
|
+
Starlette middleware. Attach it to your FastMCP HTTP app:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
app.add_middleware(
|
|
191
|
+
JWKSAuthMiddleware,
|
|
192
|
+
verifier=None, # optional: provide your own JWETokenVerifier
|
|
193
|
+
jwks_path="/.well-known/jwks.json", # public-key endpoint path
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The middleware automatically serves your server's **public** JWKS at the configured path so that token producers can discover your encryption key.
|
|
198
|
+
|
|
199
|
+
### `get_user() -> AuthUser`
|
|
200
|
+
|
|
201
|
+
Returns the authenticated user's claims for the current request. Safe to call from any async context (tools, routes, dependencies) during request handling.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
user = get_user()
|
|
205
|
+
user.email # claim value or None
|
|
206
|
+
user["role"] # also works as a regular dict
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `JWETokenVerifier`
|
|
210
|
+
|
|
211
|
+
Lower-level class if you need to verify tokens outside the middleware:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from fastmcp_auth import JWETokenVerifier
|
|
215
|
+
|
|
216
|
+
verifier = JWETokenVerifier() # reads MCP_KEY_FILE_PATH
|
|
217
|
+
claims = await verifier.verify_token(token_string)
|
|
218
|
+
public_jwks = verifier.get_jwks() # for publishing
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Environment variables
|
|
224
|
+
|
|
225
|
+
| Variable | Required | Description |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `MCP_KEY_FILE_PATH` | Yes | Path to the private JWKS JSON file |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## How the token flow works
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Producer (client/gateway) MCP Server
|
|
235
|
+
――――――――――――――――――――――――― ――――――――――
|
|
236
|
+
GET /.well-known/jwks.json
|
|
237
|
+
◄──── { public RSA key }
|
|
238
|
+
Encrypt claims with public key
|
|
239
|
+
POST /mcp/tool ──────────────────►
|
|
240
|
+
Authorization: Bearer <JWE>
|
|
241
|
+
Middleware decrypts JWE
|
|
242
|
+
with private key
|
|
243
|
+
─► get_user() returns claims
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
1. The token producer fetches the server's public key from `/.well-known/jwks.json`.
|
|
247
|
+
2. It encrypts a JSON payload (user claims) into a JWE token using that public key.
|
|
248
|
+
3. The token is sent as a `Bearer` token in the `Authorization` header.
|
|
249
|
+
4. The middleware decrypts it with the private key and exposes claims via `get_user()`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Project structure
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
fastmcp-auth/
|
|
257
|
+
├── pyproject.toml
|
|
258
|
+
├── README.md
|
|
259
|
+
├── LICENSE
|
|
260
|
+
├── CHANGELOG.md
|
|
261
|
+
├── requirements.txt
|
|
262
|
+
├── requirements-dev.txt
|
|
263
|
+
├── MANIFEST.in
|
|
264
|
+
├── .gitignore
|
|
265
|
+
├── fastmcp_auth/
|
|
266
|
+
│ ├── __init__.py
|
|
267
|
+
│ ├── py.typed
|
|
268
|
+
│ ├── cli.py
|
|
269
|
+
│ ├── middleware.py
|
|
270
|
+
│ └── verifier.py
|
|
271
|
+
├── tests/
|
|
272
|
+
└── examples/
|
|
273
|
+
├── server.py
|
|
274
|
+
└── k8s-deployment.yaml
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# fastmcp-auth
|
|
2
|
+
|
|
3
|
+
JWE authentication middleware for [FastMCP](https://github.com/jlowin/fastmcp) / Starlette servers. Encrypt user data end-to-end so that only your MCP server can read it.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`fastmcp-auth` gives your FastMCP 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** (`fastmcp-auth`) 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 fastmcp-auth
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This installs the library **and** the `fastmcp-auth` CLI.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start (local development)
|
|
25
|
+
|
|
26
|
+
### 1. Generate keys
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
fastmcp-auth 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 FastMCP server
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from fastmcp import FastMCP
|
|
54
|
+
from fastmcp_auth import JWKSAuthMiddleware, get_user
|
|
55
|
+
import uvicorn
|
|
56
|
+
|
|
57
|
+
mcp = FastMCP("My Server")
|
|
58
|
+
|
|
59
|
+
# Register your tools on `mcp` as usual …
|
|
60
|
+
|
|
61
|
+
app = mcp.http_app()
|
|
62
|
+
app.add_middleware(JWKSAuthMiddleware)
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 4. Access user claims inside any tool
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
@mcp.tool()
|
|
72
|
+
def whoami() -> str:
|
|
73
|
+
user = get_user()
|
|
74
|
+
return f"Hello, {user.name or 'anonymous'}!"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`get_user()` returns an `AuthUser` dict. Access claims as attributes — missing keys return `None` instead of raising.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Kubernetes deployment
|
|
82
|
+
|
|
83
|
+
### 1. Generate keys and pipe straight into kubectl
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
fastmcp-auth k8s | kubectl apply -f -
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
This creates a Kubernetes `Secret` named `mcp-server-keys` in the `default` namespace. Customise with flags:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
fastmcp-auth k8s --namespace my-ns --secret-name my-mcp-keys | kubectl apply -f -
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Clean up local key material
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
fastmcp-auth clean
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Uses `shred` (Linux) for secure deletion when available; falls back to overwrite-then-delete.
|
|
102
|
+
|
|
103
|
+
### 3. Mount the secret in your deployment
|
|
104
|
+
|
|
105
|
+
Add the following snippets to your existing Deployment YAML.
|
|
106
|
+
|
|
107
|
+
**Volume definition** (under `spec.template.spec.volumes`):
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
- name: mcp-secret-volume
|
|
111
|
+
secret:
|
|
112
|
+
secretName: mcp-server-keys # must match --secret-name
|
|
113
|
+
defaultMode: 0400
|
|
114
|
+
items:
|
|
115
|
+
- key: mcp_jwks
|
|
116
|
+
path: key.json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Volume mount** (under your container's `volumeMounts`):
|
|
120
|
+
|
|
121
|
+
```yaml
|
|
122
|
+
- mountPath: /etc/mcp/secrets
|
|
123
|
+
name: mcp-secret-volume
|
|
124
|
+
readOnly: true
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Environment variable** (under `env`):
|
|
128
|
+
|
|
129
|
+
```yaml
|
|
130
|
+
- name: MCP_KEY_FILE_PATH
|
|
131
|
+
value: "/etc/mcp/secrets/key.json"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A full example Deployment + Service is in [`examples/k8s-deployment.yaml`](examples/k8s-deployment.yaml).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## CLI reference
|
|
139
|
+
|
|
140
|
+
All commands accept `-o / --output <dir>` to change the key directory (default: `.keys`).
|
|
141
|
+
|
|
142
|
+
| Command | Description |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `fastmcp-auth generate` | Generate an RSA-4096 JWKS key pair |
|
|
145
|
+
| `fastmcp-auth k8s` | Generate keys and print a Kubernetes Secret YAML to stdout |
|
|
146
|
+
| `fastmcp-auth clean` | Securely delete keys from the output directory |
|
|
147
|
+
|
|
148
|
+
### `fastmcp-auth k8s` flags
|
|
149
|
+
|
|
150
|
+
| Flag | Default | Description |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `-n, --namespace` | `default` | Kubernetes namespace |
|
|
153
|
+
| `-s, --secret-name` | `mcp-server-keys` | Name of the K8s Secret |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## API reference
|
|
158
|
+
|
|
159
|
+
### `JWKSAuthMiddleware`
|
|
160
|
+
|
|
161
|
+
Starlette middleware. Attach it to your FastMCP HTTP app:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
app.add_middleware(
|
|
165
|
+
JWKSAuthMiddleware,
|
|
166
|
+
verifier=None, # optional: provide your own JWETokenVerifier
|
|
167
|
+
jwks_path="/.well-known/jwks.json", # public-key endpoint path
|
|
168
|
+
)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The middleware automatically serves your server's **public** JWKS at the configured path so that token producers can discover your encryption key.
|
|
172
|
+
|
|
173
|
+
### `get_user() -> AuthUser`
|
|
174
|
+
|
|
175
|
+
Returns the authenticated user's claims for the current request. Safe to call from any async context (tools, routes, dependencies) during request handling.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
user = get_user()
|
|
179
|
+
user.email # claim value or None
|
|
180
|
+
user["role"] # also works as a regular dict
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### `JWETokenVerifier`
|
|
184
|
+
|
|
185
|
+
Lower-level class if you need to verify tokens outside the middleware:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from fastmcp_auth import JWETokenVerifier
|
|
189
|
+
|
|
190
|
+
verifier = JWETokenVerifier() # reads MCP_KEY_FILE_PATH
|
|
191
|
+
claims = await verifier.verify_token(token_string)
|
|
192
|
+
public_jwks = verifier.get_jwks() # for publishing
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Environment variables
|
|
198
|
+
|
|
199
|
+
| Variable | Required | Description |
|
|
200
|
+
|---|---|---|
|
|
201
|
+
| `MCP_KEY_FILE_PATH` | Yes | Path to the private JWKS JSON file |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## How the token flow works
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Producer (client/gateway) MCP Server
|
|
209
|
+
――――――――――――――――――――――――― ――――――――――
|
|
210
|
+
GET /.well-known/jwks.json
|
|
211
|
+
◄──── { public RSA key }
|
|
212
|
+
Encrypt claims with public key
|
|
213
|
+
POST /mcp/tool ──────────────────►
|
|
214
|
+
Authorization: Bearer <JWE>
|
|
215
|
+
Middleware decrypts JWE
|
|
216
|
+
with private key
|
|
217
|
+
─► get_user() returns claims
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
1. The token producer fetches the server's public key from `/.well-known/jwks.json`.
|
|
221
|
+
2. It encrypts a JSON payload (user claims) into a JWE token using that public key.
|
|
222
|
+
3. The token is sent as a `Bearer` token in the `Authorization` header.
|
|
223
|
+
4. The middleware decrypts it with the private key and exposes claims via `get_user()`.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Project structure
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
fastmcp-auth/
|
|
231
|
+
├── pyproject.toml
|
|
232
|
+
├── README.md
|
|
233
|
+
├── LICENSE
|
|
234
|
+
├── CHANGELOG.md
|
|
235
|
+
├── requirements.txt
|
|
236
|
+
├── requirements-dev.txt
|
|
237
|
+
├── MANIFEST.in
|
|
238
|
+
├── .gitignore
|
|
239
|
+
├── fastmcp_auth/
|
|
240
|
+
│ ├── __init__.py
|
|
241
|
+
│ ├── py.typed
|
|
242
|
+
│ ├── cli.py
|
|
243
|
+
│ ├── middleware.py
|
|
244
|
+
│ └── verifier.py
|
|
245
|
+
├── tests/
|
|
246
|
+
└── examples/
|
|
247
|
+
├── server.py
|
|
248
|
+
└── k8s-deployment.yaml
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""FastMCP Auth - 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
|
+
"""FastMCP Auth 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: fastmcp-auth k8s | kubectl apply -f -", file=sys.stderr)
|
|
125
|
+
print(f"# Then clean local keys: fastmcp-auth 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="fastmcp-auth", description="FastMCP Auth - 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)
|
|
@@ -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,280 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastmcp-auth
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: JWE authentication middleware for FastMCP/Starlette applications
|
|
5
|
+
Author: Aurel Bartmann
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fhswf/fastmcp-auth
|
|
8
|
+
Project-URL: Documentation, https://github.com/fhswf/fastmcp-auth#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/fhswf/fastmcp-auth
|
|
10
|
+
Project-URL: Issues, https://github.com/fhswf/fastmcp-auth/issues
|
|
11
|
+
Keywords: mcp,fastmcp,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
|
+
# fastmcp-auth
|
|
28
|
+
|
|
29
|
+
JWE authentication middleware for [FastMCP](https://github.com/jlowin/fastmcp) / Starlette servers. Encrypt user data end-to-end so that only your MCP server can read it.
|
|
30
|
+
|
|
31
|
+
## What it does
|
|
32
|
+
|
|
33
|
+
`fastmcp-auth` gives your FastMCP 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** (`fastmcp-auth`) 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 fastmcp-auth
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This installs the library **and** the `fastmcp-auth` CLI.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Quick start (local development)
|
|
51
|
+
|
|
52
|
+
### 1. Generate keys
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
fastmcp-auth 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 FastMCP server
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fastmcp import FastMCP
|
|
80
|
+
from fastmcp_auth import JWKSAuthMiddleware, get_user
|
|
81
|
+
import uvicorn
|
|
82
|
+
|
|
83
|
+
mcp = FastMCP("My Server")
|
|
84
|
+
|
|
85
|
+
# Register your tools on `mcp` as usual …
|
|
86
|
+
|
|
87
|
+
app = mcp.http_app()
|
|
88
|
+
app.add_middleware(JWKSAuthMiddleware)
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. Access user claims inside any tool
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
@mcp.tool()
|
|
98
|
+
def whoami() -> str:
|
|
99
|
+
user = get_user()
|
|
100
|
+
return f"Hello, {user.name or 'anonymous'}!"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`get_user()` returns an `AuthUser` dict. Access claims as attributes — missing keys return `None` instead of raising.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Kubernetes deployment
|
|
108
|
+
|
|
109
|
+
### 1. Generate keys and pipe straight into kubectl
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
fastmcp-auth k8s | kubectl apply -f -
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This creates a Kubernetes `Secret` named `mcp-server-keys` in the `default` namespace. Customise with flags:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
fastmcp-auth k8s --namespace my-ns --secret-name my-mcp-keys | kubectl apply -f -
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. Clean up local key material
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
fastmcp-auth clean
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Uses `shred` (Linux) for secure deletion when available; falls back to overwrite-then-delete.
|
|
128
|
+
|
|
129
|
+
### 3. Mount the secret in your deployment
|
|
130
|
+
|
|
131
|
+
Add the following snippets to your existing Deployment YAML.
|
|
132
|
+
|
|
133
|
+
**Volume definition** (under `spec.template.spec.volumes`):
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
- name: mcp-secret-volume
|
|
137
|
+
secret:
|
|
138
|
+
secretName: mcp-server-keys # must match --secret-name
|
|
139
|
+
defaultMode: 0400
|
|
140
|
+
items:
|
|
141
|
+
- key: mcp_jwks
|
|
142
|
+
path: key.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Volume mount** (under your container's `volumeMounts`):
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
- mountPath: /etc/mcp/secrets
|
|
149
|
+
name: mcp-secret-volume
|
|
150
|
+
readOnly: true
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Environment variable** (under `env`):
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
- name: MCP_KEY_FILE_PATH
|
|
157
|
+
value: "/etc/mcp/secrets/key.json"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A full example Deployment + Service is in [`examples/k8s-deployment.yaml`](examples/k8s-deployment.yaml).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## CLI reference
|
|
165
|
+
|
|
166
|
+
All commands accept `-o / --output <dir>` to change the key directory (default: `.keys`).
|
|
167
|
+
|
|
168
|
+
| Command | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `fastmcp-auth generate` | Generate an RSA-4096 JWKS key pair |
|
|
171
|
+
| `fastmcp-auth k8s` | Generate keys and print a Kubernetes Secret YAML to stdout |
|
|
172
|
+
| `fastmcp-auth clean` | Securely delete keys from the output directory |
|
|
173
|
+
|
|
174
|
+
### `fastmcp-auth k8s` flags
|
|
175
|
+
|
|
176
|
+
| Flag | Default | Description |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `-n, --namespace` | `default` | Kubernetes namespace |
|
|
179
|
+
| `-s, --secret-name` | `mcp-server-keys` | Name of the K8s Secret |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## API reference
|
|
184
|
+
|
|
185
|
+
### `JWKSAuthMiddleware`
|
|
186
|
+
|
|
187
|
+
Starlette middleware. Attach it to your FastMCP HTTP app:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
app.add_middleware(
|
|
191
|
+
JWKSAuthMiddleware,
|
|
192
|
+
verifier=None, # optional: provide your own JWETokenVerifier
|
|
193
|
+
jwks_path="/.well-known/jwks.json", # public-key endpoint path
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The middleware automatically serves your server's **public** JWKS at the configured path so that token producers can discover your encryption key.
|
|
198
|
+
|
|
199
|
+
### `get_user() -> AuthUser`
|
|
200
|
+
|
|
201
|
+
Returns the authenticated user's claims for the current request. Safe to call from any async context (tools, routes, dependencies) during request handling.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
user = get_user()
|
|
205
|
+
user.email # claim value or None
|
|
206
|
+
user["role"] # also works as a regular dict
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### `JWETokenVerifier`
|
|
210
|
+
|
|
211
|
+
Lower-level class if you need to verify tokens outside the middleware:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from fastmcp_auth import JWETokenVerifier
|
|
215
|
+
|
|
216
|
+
verifier = JWETokenVerifier() # reads MCP_KEY_FILE_PATH
|
|
217
|
+
claims = await verifier.verify_token(token_string)
|
|
218
|
+
public_jwks = verifier.get_jwks() # for publishing
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Environment variables
|
|
224
|
+
|
|
225
|
+
| Variable | Required | Description |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `MCP_KEY_FILE_PATH` | Yes | Path to the private JWKS JSON file |
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## How the token flow works
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Producer (client/gateway) MCP Server
|
|
235
|
+
――――――――――――――――――――――――― ――――――――――
|
|
236
|
+
GET /.well-known/jwks.json
|
|
237
|
+
◄──── { public RSA key }
|
|
238
|
+
Encrypt claims with public key
|
|
239
|
+
POST /mcp/tool ──────────────────►
|
|
240
|
+
Authorization: Bearer <JWE>
|
|
241
|
+
Middleware decrypts JWE
|
|
242
|
+
with private key
|
|
243
|
+
─► get_user() returns claims
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
1. The token producer fetches the server's public key from `/.well-known/jwks.json`.
|
|
247
|
+
2. It encrypts a JSON payload (user claims) into a JWE token using that public key.
|
|
248
|
+
3. The token is sent as a `Bearer` token in the `Authorization` header.
|
|
249
|
+
4. The middleware decrypts it with the private key and exposes claims via `get_user()`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Project structure
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
fastmcp-auth/
|
|
257
|
+
├── pyproject.toml
|
|
258
|
+
├── README.md
|
|
259
|
+
├── LICENSE
|
|
260
|
+
├── CHANGELOG.md
|
|
261
|
+
├── requirements.txt
|
|
262
|
+
├── requirements-dev.txt
|
|
263
|
+
├── MANIFEST.in
|
|
264
|
+
├── .gitignore
|
|
265
|
+
├── fastmcp_auth/
|
|
266
|
+
│ ├── __init__.py
|
|
267
|
+
│ ├── py.typed
|
|
268
|
+
│ ├── cli.py
|
|
269
|
+
│ ├── middleware.py
|
|
270
|
+
│ └── verifier.py
|
|
271
|
+
├── tests/
|
|
272
|
+
└── examples/
|
|
273
|
+
├── server.py
|
|
274
|
+
└── k8s-deployment.yaml
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
## License
|
|
279
|
+
|
|
280
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
pyproject.toml
|
|
6
|
+
requirements.txt
|
|
7
|
+
fastmcp_auth/__init__.py
|
|
8
|
+
fastmcp_auth/cli.py
|
|
9
|
+
fastmcp_auth/middleware.py
|
|
10
|
+
fastmcp_auth/verifier.py
|
|
11
|
+
fastmcp_auth.egg-info/PKG-INFO
|
|
12
|
+
fastmcp_auth.egg-info/SOURCES.txt
|
|
13
|
+
fastmcp_auth.egg-info/dependency_links.txt
|
|
14
|
+
fastmcp_auth.egg-info/entry_points.txt
|
|
15
|
+
fastmcp_auth.egg-info/requires.txt
|
|
16
|
+
fastmcp_auth.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastmcp_auth
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastmcp-auth"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "JWE authentication middleware for FastMCP/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", "fastmcp", "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
|
+
fastmcp-auth = "fastmcp_auth.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/fhswf/fastmcp-auth"
|
|
36
|
+
Documentation = "https://github.com/fhswf/fastmcp-auth#readme"
|
|
37
|
+
Repository = "https://github.com/fhswf/fastmcp-auth"
|
|
38
|
+
Issues = "https://github.com/fhswf/fastmcp-auth/issues"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
include = ["fastmcp_auth*"]
|