带有AWS Cognito集成的MCP OAuth服务器
这个项目实现了一个OAuth 2.0授权服务器,该服务器作为MCP(模型上下文协议)客户端和AWS Cognito之间的代理,为MCP应用程序提供安全的身份验证和授权服务。
注: 本仓库中的Flask实现是基于 AWS关于部署模型上下文协议服务器的指南。
目录
概述
这个MCP OAuth服务器提供了一个符合标准的OAuth 2.0实现,其特点为:
- 起着……的作用 授权服务器代理 在MCP客户端和AWS Cognito之间
- 实施(方案/措施等) 动态客户端注册 (RFC 7591)
- 支持 授权码流程 使用PKCE(RFC 7636)
- 提供 令牌刷新 能力;才能;功能
- 用途 DynamoDB(注:DynamoDB是亚马逊提供的一项完全托管的NoSQL数据库服务,这里直接保留原名,不进行翻译) 用于会话和令牌管理
- 遵循;接着 RFC 8414 用于OAuth服务器元数据发现
为何选择这种架构?
这个代理层提供的是,让MCP客户端无需直接与Cognito集成,而是通过:
- 抽象客户无需了解Cognito的特定知识,即可通过标准的OAuth接口进行交互
- 灵活性在不更改客户端实现的情况下切换身份提供商
- 安全额外的验证和PKCE(Proof Key for Code Exchange,代码交换的证明密钥)强制执行
- 控制自定义作用域、速率限制和审计功能
- MCP特异性特征可以添加MCP特定的声明或功能
建筑
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ MCP Client │ ◄─────► │ MCP OAuth │ ◄─────► │ AWS │
│ (e.g., │ │ Server │ │ Cognito │
│ Cursor) │ │ (This Project) │ │ (IdP) │
│ │ │ │ │ │
└─────────────┘ └─────────────────┘ └──────────────┘
│
▼
┌─────────────┐
│ DynamoDB │
│ (Sessions │
│ & Tokens) │
└─────────────┘关键组件:
- MCP 客户端任何需要认证的应用程序(例如,Cursor IDE、Claude Desktop)
- MCP OAuth 服务器这个应用程序 - 处理OAuth流程并管理会话
- AWS Cognito身份提供者 - 验证用户身份并颁发令牌
- DynamoDB(亚马逊的分布式NoSQL数据库服务)客户端注册、会话和令牌映射的持久化存储
OAuth流程详解
让我们使用AWS Cognito作为身份提供商,逐步了解完整的OAuth流程。此示例展示了用户如何使用MCP客户端进行身份验证。
第一阶段:发现与注册
步骤1:元数据发现(可选)
客户端首先发现服务器的功能:
GET /.well-known/oauth-authorization-server
Host: mcp-server.example.com回应:
{
"issuer": "https://mcp-server.example.com",
"authorization_endpoint": "https://mcp-server.example.com/authorize",
"token_endpoint": "https://mcp-server.example.com/token",
"registration_endpoint": "https://mcp-server.example.com/register",
"jwks_uri": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_xxxxx/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"scopes_supported": ["openid", "email", "profile", "mcp-server/read", "mcp-server/write"],
"code_challenge_methods_supported": ["S256"]
}这告诉客户端应将授权请求发送到何处以及支持哪些功能。
步骤2:动态客户端注册
如果客户端未预先注册,则会自动进行注册:
POST /register
Content-Type: application/json
{
"client_name": "My MCP Client",
"redirect_uris": ["cursor://callback", "http://localhost:3000/callback"]
}服务器操作:
- 验证重定向URI(必须为HTTPS、localhost或自定义协议,如
cursor://) - 生成一个唯一的
client_id(通用唯一标识符) - 在DynamoDB中存储客户端信息
- 返回客户端凭据
回答:
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_id_issued_at": 1729526400,
"client_secret_expires_at": 0,
"client_name": "My MCP Client",
"redirect_uris": ["cursor://callback", "http://localhost:3000/callback"]
}第二阶段:授权流程(OAuth 舞蹈)
步骤3:授权请求
客户端通过将用户重定向到授权端点来启动OAuth流程:
GET /authorize?
client_id=550e8400-e29b-41d4-a716-446655440000
&redirect_uri=cursor://callback
&response_type=code
&state=client_state_xyz123
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&scope=openid%20email%20profile服务器操作:
- 验证请求:
- 核对以确认 client_id 存在于DynamoDB中 - 验证 redirect_uri 匹配已注册的URI(统一资源标识符) - 确保 response_type 是“代码”
- 创建一个会话:
sessionId = "7f8e9d0c-1234-5678-90ab-cdef12345678"
// Store in DynamoDB
{
client_id: "550e8400-e29b-41d4-a716-446655440000",
redirect_uri: "cursor://callback", // Client's callback
state: "client_state_xyz123", // Client's CSRF token
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
code_challenge_method: "S256",
scope: "openid email profile",
created_at: 1729526400
}- 重定向到 Cognito:
302 Redirect to:
https://my-app.auth.us-west-2.amazoncognito.com/oauth2/authorize?
client_id=YOUR_COGNITO_CLIENT_ID
&response_type=code
&redirect_uri=https://mcp-server.example.com/callback ← Server's callback!
&state=7f8e9d0c-1234-5678-90ab-cdef12345678 ← Session ID!
&scope=openid%20email%20profile关键见解发送到Cognito的重定向URI是 MCP服务器的 /callback 终端节点,而不是客户端的重定向URI。会话ID被用作Cognito的状态参数。
步骤4:在Cognito进行用户身份验证
User → Cognito Hosted UI
↓
Enters username/password
↓
Completes MFA (if enabled)
↓
Grants consent
↓
Cognito validates credentials这一步完全在Cognito中进行。用户看到的是Cognito的登录页面,并在那里进行身份验证。
步骤5:Cognito回调
成功认证后,Cognito 会重定向回 MCP 服务器:
GET /callback?
code=COGNITO_AUTH_CODE_abc123def456
&state=7f8e9d0c-1234-5678-90ab-cdef12345678
Host: mcp-server.example.com服务器操作:
- 检索会话 使用状态参数(会话ID):
session = await tokenStore.getSession("7f8e9d0c-1234-5678-90ab-cdef12345678")
// Returns:
{
client_id: "550e8400-e29b-41d4-a716-446655440000",
redirect_uri: "cursor://callback", // Original client callback
state: "client_state_xyz123", // Original client state
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
scope: "openid email profile"
}- 用Cognito代码兑换代币:
POST /oauth2/token
Host: my-app.auth.us-west-2.amazoncognito.com
Authorization: Basic base64(CLIENT_ID:CLIENT_SECRET)
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=COGNITO_AUTH_CODE_abc123def456
&redirect_uri=https://mcp-server.example.com/callback ← Must match!Cognito 响应:
{
"access_token": "eyJraWQiOiJ...",
"refresh_token": "eyJjdHkiOiJ...",
"id_token": "eyJraWQiOiJ...",
"token_type": "Bearer",
"expires_in": 3600
}- 生成MCP授权码:
mcpAuthCode = "mcp-789xyz-456abc-123def"- 在DynamoDB中存储令牌映射:
{
cognito_access_token: "eyJraWQiOiJ...",
cognito_refresh_token: "eyJjdHkiOiJ...",
cognito_id_token: "eyJraWQiOiJ...",
client_id: "550e8400-e29b-41d4-a716-446655440000",
scope: "openid email profile",
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
code_challenge_method: "S256",
expires_in: 3600,
created_at: 1729526400
}- 重定向到客户端:
302 Redirect to:
cursor://callback?
code=mcp-789xyz-456abc-123def ← MCP code, not Cognito's!
&state=client_state_xyz123 ← Original client state关键见解Cognito授权码会立即被交换为令牌,且不会离开服务器。客户端会收到一个 新的MCP授权码 这映射到存储在DynamoDB中的Cognito令牌。
步骤6:代币兑换
客户端现在将MCP授权码兑换成令牌:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=mcp-789xyz-456abc-123def
&client_id=550e8400-e29b-41d4-a716-446655440000
&redirect_uri=cursor://callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ← PKCE服务器操作:
- 检索令牌映射 使用MCP代码从DynamoDB中获取
- 验证客户端 与启动流的那一个相匹配
- 验证PKCE(Proof Key for Code Exchange,用于代码交换的证明密钥):
// Calculate SHA256 of code_verifier
calculated = SHA256("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
// Compare with stored code_challenge
if (calculated === "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM") {
// Valid!
}- 返回Cognito令牌 致客户:
{
"access_token": "eyJraWQiOiJ...",
"refresh_token": "eyJjdHkiOiJ...",
"id_token": "eyJraWQiOiJ...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile"
}- 删除授权码 来自 DynamoDB(一次性使用)
关键见解MCP服务器充当透明代理。客户端接收来自Cognito的实际令牌,但PKCE(Proof Key for Code Exchange)验证是在MCP服务器层面进行的。
步骤7:令牌刷新(可选)
当访问令牌过期时,客户端可以刷新它:
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=eyJjdHkiOiJ...
&client_id=550e8400-e29b-41d4-a716-446655440000服务器操作:
- 将刷新请求转发给Cognito:
POST /oauth2/token
Host: my-app.auth.us-west-2.amazoncognito.com
grant_type=refresh_token
&client_id=YOUR_COGNITO_CLIENT_ID
&refresh_token=eyJjdHkiOiJ...- 返回刷新后的令牌 从Cognito到客户端
视觉流程概要
┌──────────┐ ┌────────────┐ ┌─────────┐
│ Client │ │ MCP Server │ │ Cognito │
└────┬─────┘ └─────┬──────┘ └────┬────┘
│ │ │
│ 1. GET /authorize │ │
│ (client_id, redirect_uri, │ │
│ code_challenge) │ │
├───────────────────────────────►│ │
│ │ │
│ │ 2. Store session in DynamoDB │
│ │ (client details, challenge) │
│ │ │
│ │ 3. Redirect to Cognito │
│ │ (server callback, sessionID)│
│ ├───────────────────────────────►│
│ │ │
│ │ 4. User authenticates │
│ │ at Cognito │
│ │ │
│ │ 5. Callback with Cognito code │
│ │◄───────────────────────────────┤
│ │ │
│ │ 6. Get session from DynamoDB │
│ │ │
│ │ 7. Exchange code for tokens │
│ ├───────────────────────────────►│
│ │◄───────────────────────────────┤
│ │ (access, refresh, id tokens) │
│ │ │
│ │ 8. Generate MCP code │
│ │ Store token mapping │
│ │ │
│ 9. Redirect with MCP code │ │
│◄───────────────────────────────┤ │
│ │ │
│ 10. POST /token │ │
│ (MCP code, code_verifier) │ │
├───────────────────────────────►│ │
│ │ │
│ │ 11. Validate PKCE │
│ │ Get tokens from DynamoDB │
│ │ │
│ 12. Return Cognito tokens │ │
│◄───────────────────────────────┤ │
│ │ │设置与配置
先决条件
- Python 3.8+ 或 Node.js 18+
- 带有以下内容的AWS账户:
- 配置了Cognito用户池 - DynamoDB 表 - DynamoDB、SSM、Secrets Manager 的 IAM 权限
- 环境变量(见下文)
环境变量
# Cognito Configuration
COGNITO_DOMAIN=your-app-name # e.g., "my-app"
COGNITO_CLIENT_ID=abc123... # Cognito App Client ID
COGNITO_USER_POOL_ID=us-west-2_xxxxx # Cognito User Pool ID
COGNITO_CLIENT_SECRET=secret123... # Cognito App Client Secret
AWS_REGION=us-west-2 # AWS Region
# DynamoDB
TOKEN_TABLE_NAME=mcp-oauth-tokens # DynamoDB table name
# Server Configuration
MCP_SERVER_BASE_URL=https://mcp-server.example.com # Public server URL
PORT=3000 # Server port
# Security (Python version)
JWT_SECRET_KEY=your-secret-key-here # For signing JWT tokens
# Optional - SSM Parameter Store
COGNITO_SECRET_PARAM_NAME=/mcp/cognito/secret # SSM parameter path
MCP_SERVER_BASE_URL_PARAMETER_NAME=/mcp/base-urlDynamoDB 表架构
该表使用了一个由分区键(PK)和排序键(SK)组成的复合主键:
Table: mcp-oauth-tokens
Primary Key:
- PK (String) - Partition key
- SK (String) - Sort key
Attributes:
- data (Map) - Contains the actual data
- created_at (Number) - Unix timestamp
- expiration (Number) - TTL for automatic cleanup关键模式:
- 客户:
PK=CLIENT#,SK=CLIENT - 会话:
PK=SESSION#,SK=SESSION - 代币:
PK=TOKEN#,SK=TOKEN - 刷新:
PK=REFRESH#,SK=REFRESH
安装
python
pip install starlette uvicorn boto3 pyjwt requests
python flask.pyTypeScript:
npm install
npm run dev安全特性
1. PKCE(用于代码交换的证明密钥)
防止授权码拦截攻击:
- 客户端生成随机数
code_verifier - 客户端计算
code_challenge = SHA256(code_verifier) - 收到挑战
/authorize存储在会话中 - 验证器已发送
/token服务器验证匹配
2. 状态参数(CSRF保护)
防止跨站请求伪造:
- 客户端生成随机状态
- 服务器在会话中存储
- 服务器将信息回传给客户端
- 客户端验证匹配
3. 会话绑定
将授权请求链接到回调:
- 服务器生成唯一的会话ID
- 存储所有请求参数
- 用作Cognito状态参数
- 检索以获取原始客户详细信息
4. 授权码隔离
确保Cognito令牌安全:
- Cognito 代码从未到达客户端
- 立即兑换成代币
- 新生成的MCP代码
- 服务器端存储的令牌
- 一次性使用强制措施
5. 令牌映射安全
- 授权码的较短生存时间(TTL)(10分钟)
- 一次性代码(交换后删除)
- 在代币兑换时进行客户验证
- 需要进行PKCE验证
API 端点
GET /.well-known/oauth-authorization-server
OAuth 2.0 授权服务器元数据(RFC 8414)
回答: 具备服务器功能的JSON
POST /register
动态客户端注册(RFC 7591)
请求:
{
"client_name": "My App",
"redirect_uris": ["https://example.com/callback"]
}回答:
{
"client_id": "...",
"client_id_issued_at": 1234567890,
...
}GET /authorize
OAuth 2.0 授权端点
参数:
client_id(必需的)redirect_uri(必填)response_type(必填,必须为“代码”)state(推荐)code_challenge(PKCE 所需)code_challenge_method(可选,默认值为“S256”)scope(可选)
回答: 302重定向到Cognito
GET /callback
处理Cognito回调(内部端点)
参数:
code- 来自Cognito的授权码state- 会话ID
回答: 将302重定向到客户端,并附带MCP代码
POST /token
OAuth 2.0 令牌端点
授权类型:
authorization_coderefresh_token
参数(授权码):
grant_type=authorization_codecode(必填)client_id(必需)redirect_uri(必填)code_verifier(如使用 PKCE 则为必填项)
参数(刷新令牌):
grant_type=refresh_tokenrefresh_token(必需)client_id(必填)
回应:
{
"access_token": "...",
"refresh_token": "...",
"id_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile"
}数据存储
(临时)会议
目的: 将授权请求链接到回调函数 TTL:(此处“TTL”通常可翻译为“生存时间”或根据上下文具体含义翻译,但在此直接保留原英文缩写,若需具体翻译则需更多上下文) 24小时 包含: 客户端详情、PKCE(Proof Key for Code Exchange)挑战、状态
代币映射(临时)
目的: 将MCP代码映射到Cognito令牌 TTL:(在计算机网络中,TTL通常代表“Time To Live”,即生存时间) 10分钟 包含: Cognito令牌、客户端ID、PKCE挑战
客户端注册(持久性)
目的: 存储注册的客户端信息 TTL:(在计算机网络中)生存时间(Time To Live) 无(永久保存,直至删除) 包含: 客户端元数据,重定向URI
刷新令牌(长期有效)
目的: 存储刷新令牌映射 TTL:(这里“TTL”可能是一个缩写或特定术语,根据上下文可能有不同的翻译,但一般可译为)生存时间(Time To Live) 30天 包含: 令牌数据、客户端ID、作用域
故障排除
常见问题
1. 无效的redirect_uri
- 在客户端注册时确保重定向URI已注册
- URI 必须完全匹配(包括方案、主机、端口、路径)
2. 无效状态
- 会话可能已过期(有效期24小时)
- 检查DynamoDB中的会话数据
3. PKCE验证失败
- 确保 code_verifier 与 code_challenge 匹配
- 检查 code_challenge_method 是否为 "S256"
4. Cognito令牌交换失败
- 验证COGNITO_CLIENT_SECRET是否正确
- 检查回调URL是否与Cognito配置匹配
- 确保Cognito应用程序客户端设置正确
许可证
麻省理工学院(MIT)
做出贡献
欢迎贡献!请提交问题或拉取请求。
______________________________________________________________________
注: 这是一个参考实现。对于生产环境使用,请考虑:
- 速率限制
- 全面的日志记录和监控
- 令牌撤销端点
- 内省端点
- 额外的安全头部信息
- 负载均衡和高可用性
- 秘密轮换
- 全面的错误处理
