MCP 鉴权机制详解:基于 OAuth 2.0 的标准实践
前言
MCP(Model Context Protocol)作为连接 AI 助手与外部工具的桥梁,其安全性至关重要。本项目(demo)演示了一套完整的 OAuth 2.0 授权码流程 实现,采用标准 Localhost Callback 方案,让 AI 客户端(如 Claude Code)能够安全地访问受保护的 MCP 工具。
1. MCP 鉴权概述
MCP 协议本身支持多种认证机制,其中最标准的方式是借助 OAuth 2.0 授权框架。MCP 鉴权的核心目标是:
- 身份验证:确认客户端的身份(who are you)
- 授权控制:决定客户端可以访问哪些工具(what you can do)
- 会话管理:维护多次请求之间的状态(session)
2. OAuth 2.0 核心概念
2.1 角色定义
| 角色 | 说明 |
|---|---|
| Resource Owner | 资源所有者,即最终用户 |
| Client | 想要访问资源的应用程序(此处为 Claude Code) |
| Authorization Server | 颁发访问令牌的授权服务器 |
| Resource Server | 托管受保护资源的服务器(MCP 端点) |
2.2 授权码流程(Authorization Code Flow)
arduino
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌───────────┐
│ Client │ │ Browser │ │ Auth Server │ │Token Server│
└────┬────┘ └────┬────┘ └──────┬──────┘ └─────┬─────┘
│ │ │ │
│ ① 打开授权页 │ │ │
│──────────────▶│ │ │
│ │ ② 显示授权页面 │ │
│ │◀────────────────│ │
│ │ ③ 用户点击授权 │ │
│ │────────────────▶│ │
│ │ │ ④ 生成 code │
│ │◀────────────────│ 重定向 │
│ ⑤ code │ │ │
│◀─────────────│ │ │
│ │ │ │
│ ⑥ 用 code 换 token │ │
│────────────────────────────────▶│ │
│ │ │ ⑦ 返回 token │
│◀────────────────────────────────│ │
│ │ │ │
│ ⑧ 带 token 访问 MCP │ │
│────────────────────────────────────────────────────▶│
3. 标准 Localhost Callback 方案
本项目采用标准 Localhost Callback 方案,这是 OAuth 2.0 中最安全的公共客户端实现之一。
3.1 方案原理
arduino
┌────────────────────────────────────────────────────────────────────┐
│ Localhost Callback 流程 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ Client (localhost:3000) │
│ │ │
│ │ ① 启动本地 HTTP 服务器 │
│ │ │
│ │ ② 打开浏览器到 Auth Server │
│ ▼ │
│ ┌─────────────┐ Auth Server (localhost:3005) │
│ │ Browser │ │
│ └──────┬──────┘ │
│ │ │
│ │ ③ 用户授权后重定向到 │
│ │ localhost:3000/callback?code=xxx │
│ ▼ │
│ ┌─────────────┐ │
│ │ Local Server│ ← 同一台机器,同一进程 │
│ │ 收到 code │ 安全地接收到授权码 │
│ └─────────────┘ │
│ │ │
│ │ ④ code 通过内存传递(无需网络) │
│ ▼ │
│ Client 继续: │
│ │ │
│ │ ⑤ 用 code 向 /token 换取 access_token │
│ │ │
│ │ ⑥ 用 access_token 调用 MCP 端点 │
│ ▼ │
│ MCP Resource Server │
│ │
└────────────────────────────────────────────────────────────────────┘
3.2 为什么选择 Localhost Callback?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Localhost Callback | 无额外基础设施,code 在本机传递,安全 | 仅限桌面客户端 |
| Private URI Scheme | 可自定义回调协议 | 需要系统配置,可能被拦截 |
| Loopback Interface | 类似 localhost,跨平台 | 部分平台可能受限 |
4. 项目架构
4.1 整体架构图
typescript
┌─────────────────────────────────────────────────────────────────────────────┐
│ Claude Code (MCP Client) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ OAuth SDK │ │ MCP Client │ │ Transport │ │
│ │ - register │ │ - listTools │ │ - HTTP/SSE │ │
│ │ - authorize │ │ - callTool │ │ - Session │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ OAuth + MCP
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ demo Server (Express) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ OAuth Provider │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────┐ │ │
│ │ │ InMemoryClients │ │ Auth Codes │ │ Tokens │ │ │
│ │ │ Store │ │ Map │ │ Map │ │ │
│ │ └───────────────────┘ └───────────────────┘ └───────────────┘ │ │
│ │ │ │
│ │ 端点: /register, /authorize, /token, /.well-known/oauth-* │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ StreamableHTTPServerTransport │ │
│ │ • Session 管理 • Bearer Token 验证 • SSE 流式响应 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ MCP Server (McpServer) │ │
│ │ 工具: public-info (公共), protected-data (需认证) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 目录结构
bash
demo/
├── src/
│ ├── server.ts # OAuth 授权服务器 + MCP 服务器
│ └── client.ts # OAuth 客户端(MCP 消费者)
├── build/ # 编译输出
├── package.json
├── tsconfig.json
└── .env.example # 环境变量示例
5. 服务器端实现
5.1 核心组件:DemoOAuthServerProvider
typescript
class DemoOAuthServerProvider {
clientsStore = new InMemoryClientsStore();
// 授权码存储(10分钟过期)
private codes = new Map<string, {
client: OAuthClientInformationFull;
expiresAt: number;
}>();
// 访问令牌存储(1小时过期)
private tokens = new Map<string, {
clientId: string;
scopes: string[];
expiresAt: number;
}>();
// 刷新令牌存储
private refreshTokens = new Map<string, {
clientId: string;
scopes: string[];
}>();
}
5.2 授权端点(/authorize)
用户访问授权页面,确认后生成授权码并重定向:
typescript
app.get('/authorize', (req, res) => {
const { client_id, redirect_uri, state, scope } = req.query;
// 返回授权确认页面
res.send(`
<h1>Authorization Request</h1>
<form method="POST" action="/authorize/approve">
<button type="submit">Authorize</button>
</form>
`);
});
// 处理授权批准
app.post('/authorize/approve', async (req, res) => {
await oauthProvider.authorize(client, {
redirectUri: redirect_uri,
state
}, res); // 重定向到 localhost:3000/callback?code=xxx
});
5.3 Token 端点(/token)
接收授权码,返回访问令牌:
typescript
app.post('/token', async (req, res) => {
const { grant_type, code, client_id, client_secret } = req.body;
if (grant_type === 'authorization_code') {
const tokens = await oauthProvider.exchangeCodeForToken(code);
res.json(tokens); // { access_token, token_type, expires_in, ... }
}
});
5.4 MCP 端点(/mcp)------ Bearer 认证
使用 requireBearerAuth 中间件保护 MCP 端点:
typescript
app.use('/mcp',
requireBearerAuth({
verifier: oauthProvider,
requiredScopes: ['mcp:tools']
}),
mcpRouter
);
5.5 工具注册
typescript
// 公共工具 - 无需认证
server.registerTool('public-info', {
description: 'Get public server information (no auth required)'
}, async () => ({ content: [{ type: 'text', text: 'Public info' }] }));
// 受保护工具 - 需要认证
server.registerTool('protected-data', {
description: 'Get sensitive data (requires Bearer authentication)'
}, async () => ({ content: [{ type: 'text', text: 'Protected data' }] }));
6. 客户端实现
6.1 启动本地回调服务器
typescript
async function startCallbackServer(): Promise<{ code: string; server: http.Server }> {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url || '/', `http://localhost:${LOCAL_PORT}`);
if (url.pathname === '/callback') {
const code = url.searchParams.get('code');
if (code) {
res.end('<h1>Authorization Successful!</h1>');
resolve({ code, server });
}
}
});
server.listen(LOCAL_PORT);
});
}
6.2 构建授权 URL
typescript
const authUrl = new URL(`${AUTH_SERVER_URL}/authorize`);
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', LOCAL_CALLBACK_URL); // http://localhost:3000/callback
authUrl.searchParams.set('scope', 'mcp:tools');
authUrl.searchParams.set('response_type', 'code');
await open(authUrl.toString()); // 打开浏览器
6.3 交换 Token
typescript
async function getAccessToken(authCode: string): Promise<string> {
const params = new URLSearchParams({
grant_type: 'authorization_code',
code: authCode,
redirect_uri: LOCAL_CALLBACK_URL,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
});
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString()
});
const tokens = await response.json();
return tokens.access_token;
}
6.4 创建带认证的 Transport
typescript
const transport = new StreamableHTTPClientTransport(SERVER_URL, {
requestInit: {
headers: {
'Authorization': `Bearer ${accessToken}` // Bearer Token 认证
}
}
});
const client = new Client({ name: 'demo-client', version: '1.0.0' }, {});
await client.connect(transport);
// 调用工具
const tools = await client.listTools();
const result = await client.callTool({ name: 'protected-data', arguments: {} });
7. 完整数据流时序图
bash
┌───────────┐ ┌───────────┐ ┌───────────────────────────────────┐
│ Client │ │ Browser │ │ Auth Server │
└─────┬─────┘ └─────┬─────┘ └───────────────┬───────────────────┘
│ │ │
│ ① 启动本地服务器 │
│ localhost:3000 │
│ │
│ ② 打开授权页面 │
│───────────────────────────────────────────▶ GET /authorize
│ │ │
│ │ ③ 显示授权页面 │
│ │◀─────────────────────────│
│ │ │
│ │ ④ 用户点击 Authorize │
│ │─────────────────────────▶│ POST /authorize/approve
│ │ │
│ │ ⑤ 重定向到 │
│ │ localhost:3000/callback?code=xxx
│◀───────────────────────────────────────────│
│ │ │
│ ⑥ 收到 code │ │
│ (本地进程) │ │
│ │ │
│ ⑦ 用 code 换 token │
│───────────────────────────────────────────▶│ POST /token
│ │ │
│ ⑧ 收到 access_token │
│◀───────────────────────────────────────────│
│ │ │
│ ⑨ 带 Bearer Token 调用 MCP │
│───────────────────────────────────────────▶│ POST /mcp
│ │ │ Authorization: Bearer xxx
│ │ │
│ ⑩ 返回受保护数据 │
│◀───────────────────────────────────────────│
8. 安全机制
| 机制 | 说明 |
|---|---|
| Authorization Code | 临时凭证,一次性使用,10分钟过期 |
| Client Secret | 客户端身份验证,确保只有合法客户端能获取 token |
| Bearer Token | 每个请求携带,1小时过期 |
| State 参数 | 防止 CSRF 攻击(可选) |
| Localhost 回调 | code 不经过网络传输,防止拦截 |
| Scope 控制 | 细粒度权限控制(mcp:tools) |