OAuth 2.1 授权用于 Rails 应用程序中的模型上下文协议(MCP)服务器。
模型上下文协议(MCP)是一个开放标准,使AI助手能够安全地连接到外部数据源和工具。MCP Auth 实现了 MCP 授权规范,为 MCP 服务器提供基于 OAuth 2.1 的身份验证。
MCP 服务器经常需要访问用户数据或代表用户执行操作。OAuth 2.1 提供了:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ MCP 客户端 │────────▶│ 您的 Rails 应用 │◀────────│ 最终用户 │
│ (AI 助手) │ │ (MCP 服务器 + │ │ (浏览器) │
│ │ │ OAuth 服务器) │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
│ │ │
│ 1. 发现 OAuth 服务器 │ │
│────────────────────────────▶│ │
│ │ │
│ 2. 请求授权 │ │
│────────────────────────────▶│ │
│ │ │
│ │ 3. 显示同意屏幕 │
│ │────────────────────────────▶│
│ │ │
│ │ 4. 用户批准 │
│ │◀────────────────────────────│
│ │ │
│ 5. 接收授权码 │ │
│◀────────────────────────────│ │
│ │ │
│ 6. 交换令牌 │ │
│────────────────────────────▶│ │
│ │ │
│ 7. 访问 MCP 服务器 │ │
│────────────────────────────▶│ │
sequenceDiagram
participant Client as MCP 客户端
participant Server as MCP 服务器
participant AuthServer as OAuth 服务器
participant User as 最终用户
Note over Client,User: 发现阶段
Client->>Server: 请求无令牌
Server-->>Client: 401 + WWW-Authenticate 头
Client->>Server: GET /.well-known/oauth-protected-resource
Server-->>Client: 受保护资源元数据
Client->>AuthServer: GET /.well-known/oauth-authorization-server
AuthServer-->>Client: OAuth 服务器元数据
Note over Client,User: 授权阶段
Client->>Client: 生成 PKCE 挑战
Client->>User: 打开浏览器中的授权 URL
User->>AuthServer: 授权请求 (+ PKCE 挑战)
AuthServer->>User: 显示同意屏幕
User->>AuthServer: 批准访问
AuthServer-->>User: 重定向带有授权码
User->>Client: 返回授权码
Note over Client,User: 令牌交换
Client->>AuthServer: 交换码以获取令牌 (+ PKCE 验证器)
AuthServer->>AuthServer: 验证 PKCE
AuthServer-->>Client: 访问令牌 + 刷新令牌
Note over Client,User: 访问 MCP 资源
Client->>Server: MCP 请求 + Bearer 令牌
Server->>Server: 验证令牌受众
Server-->>Client: MCP 响应
PKCE (用于代码交换的证明密钥):所有授权流程都需要
资源指示符 (RFC 8707):令牌受众绑定
令牌轮换:OAuth 2.1 要求
短期访问令牌:默认 1 小时有效期
# 添加到 Gemfile
gem 'mcp-auth'
# 安装
bundle install
# 生成迁移和配置
rails generate mcp:auth:install
# 运行迁移
rails db:migrate
关键:在 config/routes.rb 的顶部挂载:
Rails.application.routes.draw do
# 首先挂载 MCP Auth - 在任何通配符路由之前
mount Mcp::Auth::Engine => '/'
# 然后是其他路由
devise_for :users
root to: 'dashboard#index'
# ... 其他路由
end
⚠️ 为什么在顶部? 该 gem 的路由(如 /.well-known/oauth-* 和 /oauth/*)需要在任何通配符路由之前注册,否则会被应用程序的路由拦截。
编辑 config/initializers/mcp_auth.rb:
Mcp::Auth.configure do |config|
# 用于 JWT 签名的 OAuth 密钥
config.oauth_secret = ENV.fetch('MCP_HMAC_SECRET', Rails.application.secret_key_base)
# 授权服务器 URL(可选 - 默认与资源服务器相同)
config.authorization_server_url = ENV.fetch('MCP_AUTHORIZATION_SERVER_URL', nil)
# MCP 服务器路径 - 您的 MCP 服务器挂载的位置
# 如果您的 MCP 服务器不在 '/mcp',请更改此设置
config.mcp_server_path = ENV.fetch('MCP_SERVER_PATH', '/mcp')
# MCP 文档 URL(可选)
# 默认:{mcp_server_path}/docs(例如,/mcp/docs)
# 可以是路径或完整 URL:
config.mcp_docs_url = ENV.fetch('MCP_DOCS_URL', nil)
# 示例:
# config.mcp_docs_url = '/docs/mcp-api'
# config.mcp_docs_url = 'https://docs.example.com/mcp'
# 令牌生命周期(秒)
config.access_token_lifetime = 3600 # 1 小时
config.refresh_token_lifetime = 2_592_000 # 30 天
config.authorization_code_lifetime = 1800 # 30 分钟
# 用户数据获取器 - 自定义此设置
config.fetch_user_data = proc do |data|
user = User.find(data[:user_id])
org = Org.find(data[:org_id]) if data[:org_id]
# 返回用户数据 + API 密钥(如果有)
{
email: user.email,
api_key_id: org&.api_key&.id,
api_key_secret: org&.api_key&.secret
}
rescue ActiveRecord::RecordNotFound
{ email: 'unknown@example.com', api_key_id: nil, api_key_secret: nil }
end
# 认证方法
config.current_user_method = :current_user
config.current_org_method = :current_org
end
您的 ApplicationController 应该有这些方法:
class ApplicationController < ActionController::Base
# 对于 Devise 用户,这些方法已经定义
# 对于自定义认证,实现这些方法:
def current_user
# 获取当前登录用户的逻辑
@current_user ||= User.find_by(id: session[:user_id])
end
def current_org
# 获取当前组织的逻辑(如果适用)
@current_org ||= current_user&.current_org
end
end
# .env
MCP_HMAC_SECRET=your_secure_random_string_here
MCP_SERVER_PATH=/mcp # 或 /api/mcp, /assistant/api 等
MCP_DOCS_URL=/docs/mcp # 可选
spring stop # 清除 spring 缓存
rails server
MCP Auth 自动提供以下端点:
GET /.well-known/oauth-protected-resource - RFC 9728 受保护资源元数据GET /.well-known/oauth-authorization-server - RFC 8414 授权服务器元数据GET /.well-known/openid-configuration - OpenID Connect 发现GET /.well-known/jwks.json - JSON Web Key Set(对于 HMAC 是空的)GET/POST /oauth/authorize - 授权端点(需要 PKCE)POST /oauth/approve - 同意批准端点POST /oauth/token - 令牌端点(授权码、刷新令牌)POST /oauth/register - 动态客户端注册(RFC 7591)POST /oauth/revoke - 令牌撤销(RFC 7009)POST /oauth/introspect - 令牌检查(RFC 7662)GET /oauth/userinfo - OpenID Connect UserInfo 端点MCP Auth 自动保护匹配您配置的 mcp_server_path 的路由:
# 如果 mcp_server_path = '/mcp'
# 所有以 /mcp/* 开头的路由都需要 OAuth 令牌
GET /mcp/tools # 受保护 ✅
GET /mcp/resources # 受保护 ✅
GET /mcp/prompts # 受保护 ✅
GET /other/endpoint # 不受保护 ❌
curl -X POST http://localhost:3000/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["https://client.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "mcp:read mcp:write"
}'
响应:
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_secret": "a1b2c3d4...",
"client_id_issued_at": 1234567890,
"client_secret_expires_at": 0,
"redirect_uris": ["https://client.example.com/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "mcp:read mcp:write"
}
生成 PKCE 参数:
// 生成 code_verifier(43-128 字符)
const codeVerifier = base64URLEncode(randomBytes(32));
// 生成 code_challenge
const codeChallenge = base64URLEncode(
sha256(codeVerifier)
);
重定向用户到授权 URL:
GET /oauth/authorize?
response_type=code&
client_id=550e8400-e29b-41d4-a716-446655440000&
redirect_uri=https://client.example.com/callback&
scope=mcp:read+mcp:write&
state=random_state_string&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256&
resource=https://example.com/mcp
用户将看到同意屏幕并批准/拒绝访问。
用户批准后,交换授权码以获取令牌:
curl -X POST https://example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTHORIZATION_CODE" \
-d "redirect_uri=https://client.example.com/callback" \
-d "code_verifier=CODE_VERIFIER" \
-d "client_id=550e8400-e29b-41d4-a716-446655440000" \
-d "resource=https://example.com/mcp"
响应:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "a1b2c3d4e5f6...",
"scope": "mcp:read mcp:write"
}
curl https://example.com/mcp/tools \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
当访问令牌过期时:
curl -X POST https://example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=a1b2c3d4e5f6..." \
-d "client_id=550e8400-e29b-41d4-a716-446655440000"
在控制器中访问认证数据:
class MyController < ApplicationController
def index
if mcp_authenticated?
user_id = mcp_user_id # 从令牌获取用户 ID
org_id = mcp_org_id # 从令牌获取组织 ID
email = mcp_email # 从令牌获取电子邮件
scope = mcp_scope # 令牌范围
api_key = mcp_api_key # 如果已配置,则获取 API 密钥
# 您的逻辑
render json: { user_id: user_id, email: email }
else
render json: { error: '未经授权' }, status: :unauthorized
end
end
end
可用的辅助方法:
mcp_authenticated? - 如果请求具有有效令牌则返回 truemcp_user_id - 从令牌获取用户 IDmcp_org_id - 从令牌获取组织 IDmcp_email - 从令牌获取电子邮件mcp_scope - 令牌范围(空格分隔的字符串)mcp_token - 访问令牌本身mcp_api_key - 如果在 fetch_user_data 中配置,则获取 API 密钥如果您的 MCP 服务器挂载在不同的路径上:
# config/initializers/mcp_auth.rb
config.mcp_server_path = '/api/v1/assistant' # 自定义路径
# 您的 MCP 服务器配置
FastMcp.mount_in_rails(
Rails.application,
path_prefix: '/api/v1/assistant' # 必须与 mcp_server_path 匹配
)
指向您的 API 文档:
# 基于路径(将在您的域前缀下)
config.mcp_docs_url = '/docs/mcp-api'
# 指向外部文档的完整 URL
config.mcp_docs_url = 'https://docs.example.com/mcp-api'
# 默认(如果没有设置):{mcp_server_path}/docs
# 示例:/mcp/docs
自定义 OAuth 同意屏幕以匹配您的品牌:
# 1. 在 config/initializers/mcp_auth.rb 中启用自定义同意视图
config.use_custom_consent_view = true
# 2. 编辑 app/views/mcp/auth/consent.html.erb
视图中可用的变量:
@client_name - 请求