MCP OAuth 2.0 认证

20 阅读4分钟

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)