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)
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌───────────┐
│ Client │ │ Browser │ │ Auth Server │ │Token Server│
└────┬────┘ └────┬────┘ └──────┬──────┘ └─────┬─────┘
│ │ │ │
│ ① 打开授权页 │ │ │
│──────────────▶│ │ │
│ │ ② 显示授权页面 │ │
│ │◀────────────────│ │
│ │ ③ 用户点击授权 │ │
│ │────────────────▶│ │
│ │ │ ④ 生成 code │
│ │◀────────────────│ 重定向 │
│ ⑤ code │ │ │
│◀─────────────│ │ │
│ │ │ │
│ ⑥ 用 code 换 token │ │
│────────────────────────────────▶│ │
│ │ │ ⑦ 返回 token │
│◀────────────────────────────────│ │
│ │ │ │
│ ⑧ 带 token 访问 MCP │ │
│────────────────────────────────────────────────────▶│
3. 标准 Localhost Callback 方案
本项目采用标准 Localhost Callback 方案,这是 OAuth 2.0 中最安全的公共客户端实现之一。
3.1 方案原理
┌────────────────────────────────────────────────────────────────────┐
│ 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 整体架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 目录结构
demo/
├── src/
│ ├── server.ts # OAuth 授权服务器 + MCP 服务器
│ └── client.ts # OAuth 客户端(MCP 消费者)
├── build/ # 编译输出
├── package.json
├── tsconfig.json
└── .env.example # 环境变量示例
5. 服务器端实现
5.1 核心组件:DemoOAuthServerProvider
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)
用户访问授权页面,确认后生成授权码并重定向:
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)
接收授权码,返回访问令牌:
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 端点:
app.use('/mcp',
requireBearerAuth({
verifier: oauthProvider,
requiredScopes: ['mcp:tools']
}),
mcpRouter
);
5.5 工具注册
// 公共工具 - 无需认证
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 启动本地回调服务器
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
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
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
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. 完整数据流时序图
┌───────────┐ ┌───────────┐ ┌───────────────────────────────────┐
│ 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) |