使用说明
MCP Janus
OAuth 2.1代理,为MCP服务器提供企业级安全性,而无需接触任何服务器代码。
问题
大多数MCP代理以错误的方式解决了身份验证问题:它们从授权服务器接收真实的IdP JWT,并将其直接传递给MCP客户端。客户端现在可以解码该JWT,读取每个声明,根据IdP重放它,并发现您的身份提供者的内部——这直接违反了 MCP授权规范,明确禁止转发不是为代理本身颁发的令牌。
安全影响是真实的:
- 客户端了解您的IdP URL、租户、受众和用户声明
- 被盗令牌可用于代理 和 上游IdP
- “代理令牌”和“其他所有令牌”之间没有界限
Janus做什么
Janus坐在任何MCP服务器前面,代表客户端运行完整的OAuth 2.1+PKCE流程。在与真实IdP交换授权码后 使用AES-256-GCM对IdP JWT进行加密 并给客户端一个不透明的blob。在每个后续请求中,它都会解密、验证并向上游转发真实的JWT——不可见。
客户端永远不会看到、解码或重放真实的令牌。零令牌传递。完全符合MCP规范。
MCP Client MCP Janus Proxy Upstream MCP Server
│ │ │
│ Authorization: Bearer <opaque> │ │
│ ──────────────────────────────────>│ │
│ │ 1. Decrypt opaque token (AES-GCM) │
│ │ 2. Validate JWT (exp, aud, iss) │
│ │ 3. Map claims → HTTP headers │
│ │ │
│ │ Authorization: Bearer <real JWT> │
│ │ X-Sub: user123 │
│ │ ──────────────────────────────────>│
│ │ │
│ │◄──────────────────────────────────│
│◄──────────────────────────────────│ │
这是给谁的?
- 平台工程师 在生产环境中部署需要真正安全性而非管道磁带身份验证的MCP服务器
- 企业团队 将Claude或ChatGPT与IdP背后的内部工具(Azure AD B2C、Okta、Keycloak、Auth0)集成
- MCP服务器开发人员 他们希望OAuth 2.1合规性,而无需从头开始重新实现身份验证
- 安保团队 审计AI集成以检测令牌泄漏和规范合规性
主要特点
安全
- 不透明加密令牌 --AES-256-GCM(AEAD)封装了每个IdP JWT;客户端只能看到密文
- 无令牌传递 --代理发行自己的令牌,从不向上游转发客户端令牌
- JWT验证 --通过JWKS密钥提取和自动轮换进行完整的声明验证(到期、受众、发行人)
- 标题映射声明 --可配置的IdP声明注入上游HTTP标头
- 加密的客户端凭据 --动态注册返回AEAD加密
client_id/client_secret - 自行发行代币模式 --Janus可以为不支持令牌刷新的MCP客户端(如Claude和ChatGPT)颁发自己的长期令牌(可配置TTL)
标准合规
- OAuth 2.1+PKCE --带有S256码挑战的授权码流;公共客户(否
client_secret)完全支持 - RFC 7591 --带有完整§3.2.1响应的动态客户端注册
- RFC 8414 --OAuth 2.0授权服务器元数据(
/.well-known/oauth-authorization-server) - RFC 9728 --受保护的资源元数据,包括
bearer_methods_supported: ["header"] - OpenID连接发现 —
/.well-known/openid-configuration - RFC 9207 —
iss授权响应中的参数(AS混淆保护)
运营
- 单个二进制 —
go build生成一个静态二进制文件,零运行时依赖关系 - 开放遥测 --分布式跟踪和度量(Jaeger、Prometheus、Grafana开箱即用)
- Docker Compose --一个命令代理+完整的可观察性堆栈
- 结构化日志记录 --JSON日志,可配置级别
- 优雅关闭 --SIGTERM上的清洁连接排水
- CORS支持 --选择加入基于浏览器的MCP客户端(例如MCP Inspector)
快速开始
选项A——使用Keycloak进行本地测试(建议首次运行)
git clone https://github.com/maurik77/mcp-janus.git
cd mcp-janus
# Start Keycloak + MCP test server
docker compose -f docker-compose.keycloak.yaml up -d
# Bootstrap realm, client, and test user — writes .env.keycloak-dev
./scripts/keycloak/setup-keycloak.sh # Linux/macOS
# .\scripts\keycloak\setup-keycloak.ps1 # Windows (PowerShell)
# Build and run the proxy
task build
cp config.keycloak-dev.yaml config.yaml
source .env.keycloak-dev && CONFIG_PATH=. ./bin/mcpproxy
# Run the full end-to-end test (opens browser for login)
./scripts/keycloak/test-proxy-flow.sh
看 docs/guide_keyclok.md 获取完整的Keycloak设置指南,包括Windows(PowerShell)说明。
选项B——带上自己的身份证
git clone https://github.com/maurik77/mcp-janus.git
cd mcp-janus
go mod download
go build -o bin/mcpproxy ./cmd/proxy
# Edit config.yaml with your IdP's OIDC discovery URL and client credentials
export MCP_IDP_CLIENT_SECRET="your-idp-client-secret"
CONFIG_PATH=. ./bin/mcpproxy
或使用 任务 快捷方式:
task install # go mod download + verify
task build # build → ./bin/mcpproxy
task run # build + run (needs MCP_IDP_CLIENT_SECRET)
验证它是否正在运行
curl http://localhost:8080/health
# OK
curl http://localhost:8080/.well-known/oauth-protected-resource | jq .
运作原理
标准不透明令牌流
- 注册 --客户电话
POST /register使用重定向URI。代理返回加密的AEADclient_id和client_secret(RFC 7591§3.2.1)。 - 授权 --客户端重定向到
GET /auth使用PKCEcode_challenge.Proxy重定向到真实的IdP。 - 回调 --IdP重定向回
GET /callbackProxy接收授权码。 - 代币兑换 --客户电话
POST /token随着code_verifier代理与IdP进行交换,接收真实的JWT,用AES-256-GCM对其进行加密,并向客户端返回一个不透明的承载。 - 经过身份验证的请求 --客户端发送
Authorization: Bearer <opaque>到/mcp/*.Proxy解密、验证JWT、将声明映射到标头,并使用真实令牌进行转发。 - 刷新 --客户电话
POST /refresh使用加密的刷新令牌。代理解密,用IdP刷新,重新加密,并返回一个新的不透明承载。
自行发行代币模式(token_behavior: self_issued)
一些MCP客户端(Claude、ChatGPT)只完成一次OAuth流,从不调用 /refresh。默认情况下 proxy 模式下,会话在IdP令牌到期时到期(通常为1小时)。这 self_issued 模式解决了这个问题:
- 在初始IdP交换后,验证JWT 一次 并提取索赔。
- Janus发行自己的不透明代币,其中包含 加密映射声明 以及Janus控制的到期日(
token_ttl). - 在每次后续请求中,代理都会解密令牌,检查到期时间,并将声明作为标头注入-- 没有JWKS呼叫,没有IdP联系人.
- 如果
/refresh如果被调用,则从相同的加密声明中发出新的访问令牌,直到token_max_ttl而无需触摸IdP。
权衡:
proxy |
self_issued |
|
|---|---|---|
| 令牌寿命 | IdP受控(例如1小时) | Janus受控(例如720小时) |
| IdP撤销在 | ~1小时内生效 | 最多 token_ttl |
| JWKS按请求调用 | 是(缓存) | 否 |
| 声明新鲜度 | 每次请求 | 登录时冻结 |
| 没有刷新支持的客户端 | 会话每小时过期 | 已满 token_ttl 持续时间 |
令牌加密
- 算法:AES-256-GCM(AEAD——具有相关数据的认证加密)
- 过程:真正的JWT→ 用256位主密钥加密→ 每次操作的随机随机数→ base64url编码→ 不透明字符串
- 解密:提取载体→ base64url解码→ 解密→ 解析JWT→ 验证索赔
索赔映射
IdP JWT声明映射到每个代理请求的上游HTTP标头:
idp:
claims_mapping:
sub: X-Sub
name: X-Full-Name
email: X-Email
upn: X-UPN
上游MCP服务器接收干净的HTTP标头——没有JWT解析,没有IdP依赖性。
配置
创建 config.yaml 在工作目录中(或使用 MCP_-前缀环境变量):
proxy:
base_url: http://localhost:8080 # Canonical URL of this proxy
listen_addr: ":8080"
log_level: info # trace|debug|info|warn|error
log_format: json
cors:
enabled: false # true for browser clients (e.g. MCP Inspector)
allowed_origins:
- http://localhost:6274
token_behavior: proxy # proxy (default) | self_issued
token_ttl: 24h # [self_issued] lifetime of each access token
token_max_ttl: 168h # [self_issued] max window from original login
idp:
client_id: your-idp-client-id
client_secret: "" # use MCP_IDP_CLIENT_SECRET env var
openid_configuration_url: https://auth.example.com/.well-known/openid-configuration
scopes:
- openid
- profile
- email
claims_mapping:
sub: X-Sub
name: X-Full-Name
email: X-Email
jwt_leeway: 10s
encryption:
# Generate with: openssl rand -hex 32
master_key: "your-64-char-hex-key"
upstream:
name: my-mcp-server
resource: https://mcp.example.com # resource indicator for audience binding
base_url: https://mcp.example.com
path_prefix: /mcp
telemetry:
enabled: true
service_name: mcp-proxy
otlp_endpoint: localhost:4318
环境变量覆盖:
export MCP_IDP_CLIENT_SECRET="your-secret"
export MCP_PROXY_BASE_URL="https://proxy.example.com"
export MCP_ENCRYPTION_MASTER_KEY="$(openssl rand -hex 32)"
export MCP_PROXY_CORS_ENABLED=true
export MCP_TOKEN_BEHAVIOR=self_issued
export MCP_TOKEN_TTL=720h
看 .env.示例 查看完整列表。
API终点
| 方法 | 路径 | 描述 |
|---|---|---|
GET |
/.well-known/openid-configuration |
OpenID连接发现 |
GET |
/.well-known/oauth-authorization-server |
授权服务器元数据(RFC 8414) |
GET |
/.well-known/oauth-protected-resource |
受保护的资源元数据(RFC 9728) |
POST |
/register |
动态客户端注册(RFC 7591) |
GET |
/auth |
使用PKCE进行OAuth授权 |
GET |
/callback |
来自IdP的OAuth回调 |
POST |
/token |
授权码→ 不透明载体 |
POST |
/refresh |
刷新令牌交换 |
GET/POST |
/mcp/* |
经过身份验证的MCP代理 |
GET |
/health |
健康检查 |
示例:注册客户
curl -s -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}' | jq .
看 文档/测试指南.md 对于完整的卷曲序列。
可观测性
docker compose -f docker-compose.observability.yaml up -d
启动Jaeger(跟踪)、Prometheus(指标)、Grafana(仪表板)和OpenTetry收集器。代理在以下情况下自动导出 telemetry.enabled: true.
关键指标:
| 度量 | 描述 |
|---|---|
mcp.proxy.auth.requests.total |
按结果验证请求 |
mcp.proxy.token.exchange.duration |
令牌交换延迟 |
mcp.proxy.requests.total |
按方法/路径/状态列出的代理请求 |
mcp.proxy.upstream.errors.total |
上游错误计数器 |
看 docs/opentelemetry.md 用于仪表板设置。
码头工人
# Proxy + MCP test server
docker compose up -d
# Full observability stack
docker compose -f docker-compose.observability.yaml up -d
# Both together
docker compose -f docker-compose.yaml -f docker-compose.observability.yaml up -d
# Keycloak dev environment
docker compose -f docker-compose.keycloak.yaml up -d
部署
这 deploy.sh 通过Helm一步完成脚本构建、标记、推送和部署:
export REGISTRY=myregistry.azurecr.io
./deploy.sh 1.0.0
执行的步骤: docker build → 标签+推送到注册表→ 更新 deployment/values-dev.yaml → helm upgrade.
贡献
- 分叉仓库并创建功能分支
- 跑
task fmt在承诺之前 - 添加新功能测试(首选表驱动)
- 确保
task test和task lint通过 - 打开一个带有清晰描述的拉取请求
如果你正在测试一个真正的IdP 钥匙斗篷设置指南 在5分钟内为您提供本地IdP。
参考文献
MCP规格
OAuth标准
- OAuth 2.1(IETF草案)
- RFC 7591:动态客户端注册
- RFC 8414:授权服务器元数据
- RFC 8707:资源指示器
- RFC 9207:AS发卡机构标识
- RFC 9728:受保护的资源元数据