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)

相关推荐
ZzT3 小时前
AI-SDLC 实践 05|开发阶段的工程结构:在一个主仓里跨多个仓库写代码
ai编程·claude
boombb3 小时前
AI Engineer in robotic系列:从地图导入到机器人执行:导览平台 MVP 技术设计与落地
机器人·ai编程
爱勇宝4 小时前
Claude Fable 5.2 发布后,开发者该关注什么
程序员·ai编程
挖掘狂人4 小时前
AI 把代码写得飞快,然后呢?聊聊我验收生成代码的一套流程
人工智能·程序员·ai编程
莪_幻尘4 小时前
4 个 3D 游戏,总体积 3.82MB:拆解 Claude 的 one-shot 工程链路
前端·ai编程·three.js
lucas_AI5 小时前
22 个开源项目公开拒绝 AI 代码:vibe coding 的账单,维护者不想买了
开源·ai编程
唐老板6 小时前
Postgres 慢查询,先取证再动手
数据库·ai编程
聪明的笨猪猪19 小时前
实际工程问题:AI 生成 Spring Cache(@Cacheable / @CachePut / @CacheEvict)缓存注解踩坑
java·经验分享·面试·ai编程
Hone19 小时前
juejin-skills 系列 ②:我把「看掘金热榜」也做成了 Agent 技能
开源·node.js·ai编程
小虎AI生活21 小时前
Anatomy 接入 WorkBuddy 实操:一句话生成 12 零件可交互科普模型(附2组提示词)
aigc·ai编程