JWE authentication middleware for HTTP MCP servers. Encrypt user data end-to-end so that only your MCP server can read it.
mcp-auth-middleware gives your MCP server two things:
- A middleware that decrypts a JWE Bearer token, enforces configured JWT scopes, and exposes the authenticated user's claims via
get_user(). - 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.
The middleware also publishes:
/.well-known/jwks.jsonfor public key discovery/.well-known/openid-configurationfor OpenID discovery
pip install mcp-auth-middlewarepython -m pip install -r requirements.txt
python -m pip install -e .
pytest --cov=mcp_auth_middleware --cov-report=term-missing --cov-fail-under=80mcp-auth-middleware generateThis writes:
.keys/mcp-private.json
.keys/mcp-public.json
MCP_KEY_FILE_PATH=.keys/mcp-private.jsonimport uvicorn
from fastmcp import FastMCP
from mcp_auth_middleware import JWKSAuthMiddleware, get_user
mcp = FastMCP("My Server")
required_scopes = [
{"scope": "name"},
{"scope": "email"},
]
@mcp.tool()
def whoami() -> str:
user = get_user()
return f"Hello, {user.name}!"
app = mcp.http_app()
app.add_middleware(JWKSAuthMiddleware, scopes=required_scopes)
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)GET /.well-known/openid-configuration returns the issuer, JWKS URI, and configured scopes:
{
"issuer": "http://localhost:8000",
"jwks_uri": "http://localhost:8000/.well-known/jwks.json",
"scopes_supported": ["name", "email"]
}The endpoint is public and includes permissive CORS headers.
If a verified token is missing one or more configured fields, the middleware rejects the request with 403 Forbidden:
{
"error": "missing_scopes",
"missing": [{ "scope": "email" }]
}Attach it to any Starlette-based MCP server app:
app.add_middleware(
JWKSAuthMiddleware,
scopes=[
{"scope": "name"},
],
verifier=None,
jwks_path="/.well-known/jwks.json",
openid_configuration_path="/.well-known/openid-configuration",
issuer=None,
)Rules:
scopesis required and must contain at least one scope.- Every configured scope is mandatory.
- Scope names must match JWT field names.
Returns the authenticated user's claims for the current request.
user = get_user()
user.email
user["email"]Optional helper dataclass for typed configuration:
from mcp_auth_middleware import ScopeDefinition
scope = ScopeDefinition(
scope="email",
)Lower-level verifier if you need token verification outside the middleware:
from mcp_auth_middleware import JWETokenVerifier
verifier = JWETokenVerifier()
claims = await verifier.verify_token(token_string)
public_jwks = verifier.get_jwks()verify_token() returns None when token decryption fails or when the decrypted payload is not a JSON object.
/.well-known/openid-configuration already includes CORS headers.
If browser clients also need /.well-known/jwks.json, add CORS middleware after JWKSAuthMiddleware:
from starlette.middleware.cors import CORSMiddleware
app.add_middleware(JWKSAuthMiddleware, scopes=required_scopes)
app.add_middleware(CORSMiddleware, allow_origins=["*"])Generate a Secret manifest:
mcp-auth-middleware k8s | kubectl apply -f -Clean up local key material:
mcp-auth-middleware cleanMount the generated private key and set:
MCP_KEY_FILE_PATH=/etc/mcp/secrets/key.jsonA full example Deployment + Service is in examples/k8s-deployment.yaml.
| Variable | Required | Description |
|---|---|---|
MCP_KEY_FILE_PATH |
Yes | Path to the private JWKS JSON file |
MIT