MCP OAuth 2.0 认证

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)

相关推荐
aqi008 小时前
15天学会AI应用开发(十七)使用LangGraph实现会话记忆功能
人工智能·python·大模型·ai编程·ai应用
kyriewen10 小时前
我扒了 GPT-5.6 的全部编程跑分——有一项 OpenAI 没敢放出来
前端·gpt·ai编程
wangruofeng11 小时前
11 万 Star 的生成式 AI 入门课,Microsoft 做对了什么
github·aigc·ai编程
甲维斯11 小时前
差距太大了!GPT5.6Sol 正面战 Fable5,啥也不是!
人工智能·ai编程
ServBay12 小时前
你还在手写 Prompt?聪明的人早就用上了循环工程,AI 的自动驾驶时代来了
ai编程·claude·mcp
子昕12 小时前
GPT-5.6上线,ChatGPT和Codex正式合并:新版完整实测
ai编程
程序员老刘14 小时前
Opencode从Tauri切回Electron,桌面端技术选型别被体积陷阱带偏了
electron·ai编程
七牛开发者14 小时前
专访 DeepChat 作者们:聊聊本地优先、MCP 与 Agent Memory
agent·ai编程·mcp
谭光志14 小时前
AI 是怎么操作浏览器的——browser use 实现原理
前端·javascript·ai编程