随着 MCP (Model Context Protocol) 生态的日渐成熟,统一且安全的认证授权标准变得至关重要。OAuth 2.0 作为业界公认的授权框架,凭借其安全性、灵活性和广泛的应用,成为了 MCP 协议认证的官方选择。
对于开发者而言,仅仅使用现成的客户端(如 Inspector 工具)可能无法满足定制化、自动化的需求。我们需要构建自己的 Agent 或工具来与 MCP 服务器交互,这就要求我们必须深入理解并能以代码实现 OAuth 2.0 的认证流程。
在开始实战之前,我们必须先理清 OAuth 2.0 场景中的关键角色,尤其是 MCP 服务器扮演的独特角色。
- 资源所有者 (Resource Owner):你,即最终用户。你拥有需要被访问的数据。这些数据可能在 MCP 服务器上(如工具使用权限),也可能在第三方服务上(如你的阿里云 OSS 存储桶或 GitHub 仓库)。
- 客户端 (Client):你正在开发的 Agent 程序。它希望代表你(资源所有者)去访问 MCP 服务器提供的工具和资源。
- MCP 服务器 (The MCP Server):这是核心,它扮演着三重角色:
- 资源服务器 (Resource Server):它托管着受保护的 MCP 工具 (
https://mcp.example.com/mcp
),客户端需要凭令牌才能调用这些工具。 - 授权服务器 (Authorization Server):从客户端的视角来看,MCP 服务器就是授权服务器。它负责处理客户端的注册、颁发和刷新自己的 MCP 访问令牌。它提供了
/register
等端点。 - 第三方服务的客户端 (Client for Third-Party Services):当 MCP 工具需要访问你在第三方(如 GitHub、阿里云)的资源时,MCP 服务器会扮演客户端的角色,去请求这些第三方服务的授权。
- 资源服务器 (Resource Server):它托管着受保护的 MCP 工具 (
这种设计被称为委托授权(Delegated Authorization)。你授权客户端访问 MCP 服务器,然后你再授权 MCP 服务器访问第三方服务。这样做的好处是:
- 安全:你的客户端 Agent 只需持有 MCP 的令牌,无需接触和管理多个第三方服务的敏感令牌。
- 解耦:MCP 服务器封装了与第三方服务交互的复杂逻辑,客户端只需与统一的 MCP 接口交互。
我们即将实践的流程是 OAuth 2.0 中最推荐、最安全的 授权码流程 (Authorization Code Flow),并结合了 PKCE (Proof Key for Code Exchange) 扩展。
1 初次试探与 401 “Unauthorized”
一切始于一次普通的 API 调用。我们尝试像访问一个开放 API 一样,向 MCP Server 发送一个 initialize
请求。
curl --location --request POST 'https://mcp.example.com/mcp' \
--header 'Content-Type: application/json' \
--data-raw '{
"jsonrpc": "2.0",
"method": "initialize",
"id": 0
}'
服务器返回了 401 Unauthorized
状态码和如下响应体,这在我们的预料之中:
{
"error": "Authorization header is missing"
}
401 Unauthorized
是一个明确的信号:" 你需要先证明你是谁,并获得许可 “。- 响应体中的
Authorization header is missing
直接告诉我们,资源服务器期望在请求头中看到一个Authorization
字段,这通常用于承载访问令牌 (Access Token)。 - 根据 HTTP 规范,一个标准的 401 响应还会包含一个
WWW-Authenticate
头,它会指明认证方案(例如Bearer
),有时还会提供授权服务器的地址,引导客户端开始认证流程。
2 服务发现
既然需要认证,我们首先要找到授权服务器在哪,以及它支持哪些功能。OAuth 2.0 授权服务器元数据规范 (RFC 8414) 定义了一个标准的 " 发现 " 端点。
我们访问这个 /.well-known/oauth-authorization-server
路径:
curl --location --request GET 'https://mcp.example.com/.well-known/oauth-authorization-server'
服务器返回了一个包含其所有能力和端点信息的 JSON 对象:
{
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"revocation_endpoint": "https://mcp.example.com/revoke",
"registration_endpoint": "https://mcp.example.com/register",
"token_endpoint_auth_methods_supported": ["none"],
"response_modes_supported": ["query"],
"issuer": "https://mcp.example.com",
"authorization_endpoint": "https://auth.example.com/oauth2/v1/auth",
"token_endpoint": "https://auth.example.com/v1/token"
}
这个 JSON 就是授权服务器的 " 说明书 “。对我们而言,最重要的几个字段是:
authorization_endpoint
: 我们将引导用户到这个地址进行登录和授权。请注意,这个地址指向了第三方授权服务! 这正是委托授权模式的体现。token_endpoint
: 我们的 Agent 将用授权码在这个地址换取访问令牌。registration_endpoint
: 我们的 Agent 需要先到这个地址 " 注册 “,以获取一个客户端身份(client_id
)。code_challenge_methods_supported
:["S256"]
告诉我们,服务器支持并期望我们使用 PKCE 的 SHA-256 方式。grant_types_supported
:["authorization_code", "refresh_token"]
表明服务器支持标准的授权码流程,并允许我们后续使用刷新令牌。token_endpoint_auth_methods_supported
:["none"]
这是一个关键信息,意味着它支持 " 公共客户端 “,这类客户端在交换令牌时无需提供client_secret
。这完全符合我们 Agent 的情况。
3 动态客户端注册
curl --location --request POST 'https://mcp.example.com/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"redirect_uris": [ "http://127.0.0.1:6274/oauth/callback" ],
"token_endpoint_auth_method": "none",
"grant_types": [ "authorization_code", "refresh_token" ],
"response_types": [ "code" ],
"client_name": "My Agent",
}'
注册成功后,授权服务器会返回客户端的凭证信息:
{
"client_id": "xxx",
"client_name": "My Agent",
"client_secret": "",
"created_at": 1752218883279,
"expires_at": 1783754883279,
"grant_types": [ "authorization_code", "refresh_token" ],
"redirect_uris": [ "http://127.0.0.1:6274/oauth/callback" ],
"response_types": [ "code" ],
"token_endpoint_auth_method": "none"
}
redirect_uris
: 这是 OAuth 流程中至关重要的安全参数。授权服务器在完成用户授权后,只会将授权码发送到这个列表中的地址。这可以防止授权码被劫持并发送到恶意攻击者的服务器。在我们的 Agent 中,我们需要启动一个本地 Web 服务来监听这个地址。token_endpoint_auth_method: "none"
: 我们明确声明自己是公共客户端。因此,返回的client_secret
是空的。grant_types
: 我们声明希望使用授权码和刷新令牌。client_id
: 这是我们客户端的唯一公共标识符。我们需要保存它,后续每一步都会用到。
4 动态生成 PKCE 代码对
为了防止 " 授权码劫持攻击 “,我们必须使用 PKCE。即使攻击者在不安全的网络环境中(例如,在操作系统的应用间通信中)截获了我们的授权码,但没有 code_verifier
,他也无法用它来交换令牌。
PKCE 的核心是创建一对密钥:一个私有的 code_verifier
和一个公开的 code_challenge
。
code_verifier
: 一个高熵的随机字符串,由我们的 Agent 生成并秘密保存。code_challenge
: 对code_verifier
进行SHA256
哈希运算,然后进行Base64Url
编码。
# 生成一个足够安全的随机字符串作为 code_verifier
code_verifier = secrets.token_urlsafe(64)
# 对 code_verifier 进行 SHA256 哈希
hashed = hashlib.sha256(code_verifier.encode('utf-8')).digest()
# 对哈希结果进行 Base64Url 编码,生成 code_challenge
code_challenge = base64.urlsafe_b64encode(hashed).decode('utf-8').rstrip('=')
# 保存这两个变量
print(f"Code Verifier: {code_verifier}")
print(f"Code Challenge: {code_challenge}")
重要提示:code_verifier
是整个流程中的一个关键秘密,必须妥善保管,直到令牌交换步骤完成。
5 发起授权请求
现在,万事俱备。我们将构造一个特殊的 URL,并引导用户(资源所有者)在浏览器中打开它并授权,授权了 Agent 能够访问 MCP Server,MCP Server 又能拿着这个 Token 去访问阿里云、Github 等第三方服务。
【Authorization Endpoint】?response_type=code&client_id=【Client ID】&code_challenge=【Code Challenge】&code_challenge_method=S256&redirect_uri=【Redirect URI】
流程拆解:
- 启动本地服务器: 你的 Agent 程序需要启动一个临时的 Web 服务器,监听注册时提供的
redirect_uri
。 - 打开构造的请求地址: 浏览器打开真正的第三方授权页面(如
auth.example.com
)。 - 用户授权: 用户在熟悉的第三方页面(如阿里云)登录,并同意授权 “MCP Server” (而不是你的 “My Agent”) 访问其云资源。
- 重定向回调地址: 授权成功后,第三方服务将浏览器重定向到预设的回调地址,并附上一个临时授权码。只要这个地址能被用户浏览器访问即可!!!
- MCP 服务器处理并重定向回客户端: MCP 服务器用这个码在后台换取了第三方令牌并安全存储。然后,它会生成一个属于 MCP 自己的新
code
,并将浏览器重定向到我们客户端指定的redirect_uri
。http://127.0.0.1:6274/oauth/callback?code=【MCP生成的Code】
- 捕获授权码: 我们的本地服务器收到这个请求,成功捕获 MCP 的授权码。
6 交换令牌
这是最后也是最关键的一步。我们的 Agent 在后台(无需用户参与)向 token_endpoint
发起一个 POST 请求,用刚刚获取的 code
和之前秘密保存的 code_verifier
来交换最终的访问令牌。
curl --location --request POST 'https://mcp.example.com/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=【Code】' \
--data-urlencode 'redirect_uri=【Redirect URI】' \
--data-urlencode 'client_id=【Client ID】' \
--data-urlencode 'code_verifier=【Code Verifier】'
当授权服务器收到此请求时,它会:
- 找到
code
对应的code_challenge
。 - 用
S256
算法处理请求中提供的code_verifier
。 - 比较计算结果和存储的
code_challenge
是否一致。 - 只有在一致的情况下,才认为请求是合法的,并颁发令牌。
如果一切顺利,服务器将返回包含令牌的 JSON 响应:
{
"scope": "/internal/acs/openapi openid aliuid",
"request_id": "ffa5bfba-8bd1-415a-b338-82928f67f314",
"access_token": "xxx",
"token_type": "Bearer",
"refresh_token": "xxx",
"id_token": "xxx",
"additional_information": {
"refresh_token_id": 16857050
},
"expires_in": 259199
}
grant_type=authorization_code
: 表明我们正在用授权码交换令牌。code_verifier
: 这就是我们用来开锁的 " 钥匙 “。它证明了发起授权请求和发起令牌交换请求的是同一个客户端。access_token
: 访问令牌。这就是我们梦寐以求的凭证!它通常是短暂的(expires_in
表示其有效期,单位为秒)。refresh_token
: 刷新令牌。它通常有更长的有效期(例如几天或几个月)。当access_token
过期后,我们可以用它来获取新的access_token
,而无需再次打扰用户。必须像密码一样安全地存储它。token_type
: “Bearer” 意味着任何持有此令牌的人(“bearer”)都可以用它来访问资源。
7 使用令牌访问 MCP Server
现在,我们可以带着 access_token
重新访问 MCP Server 了。我们将令牌放在 Authorization
请求头中,并使用 Bearer
方案。
from fastmcp import Client
from fastmcp.client.auth import BearerAuth
auth_handler = BearerAuth(access_token)
async with Client(MCP_SERVER_BASE_URL, auth=auth_handler) as client:
print("\n✅ 认证成功,已连接到服务器!")
tools = await client.list_tools()
print("\n🛠️ 服务器可用工具列表:")
if not tools:
print(" - 未发现任何工具。")
else:
for tool in tools:
print(f" - {tool.name}: {tool.description}")
当然,我们也可以构造符合 MCP 规范的 JSONRPC 2.0 格式的 POST 请求,这次会返回 200 OK 响应。
8 刷新访问令牌
当 access_token
过期后,我们的请求会再次收到 401 Unauthorized
。此时,我们不必让用户重新走一遍授权流程,而是可以使用 refresh_token
来静默地获取新的访问令牌。
curl --location --request POST 'https://mcp.example.com/v1/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=【Refresh Token】' \
--data-urlencode 'client_id=【Client ID】'
服务器验证 refresh_token
和 client_id
后,会返回一组新的 access_token
和 refresh_token
(可选)。
这个流程体现了 OAuth 2.0 的核心思想——委托授权。用户(资源所有者)从未将自己的密码暴露给我们的 Agent(客户端),而是通过一个受信任的中间方(授权服务器)授予了有限的、可撤销的访问权限。这正是 OAuth 2.0 强大而安全的原因。希望这篇详尽的指南能为你构建强大的 MCP 工具扫清障碍,并让你对现代网络认证体系有更深刻的认识。
9 保密客户端 vs. 公共客户端
- 传统 Web 应用模式(保密客户端 - Confidential Client)
- 核心假设: 客户端是一个可以安全保管机密的后端服务器。
- 认证关键: 在第 6 步 " 交换令牌 " 时,客户端必须同时提供
code
和它在注册时获得的client_secret
。client_secret
就像是客户端的密码,用于向授权服务器证明 " 我就是那个注册过的应用 “。 - 安全性:
client_secret
的存在,使得即使code
被截获,攻击者没有client_secret
也无法换取令牌。 - 适用于任何有安全后端服务器的应用。例如传统网站(如电商网站、社交媒体的后端)。
- 现代公共客户端模式(PKCE - Public Client)
- 核心假设: 客户端是无法安全保管机密的环境,如桌面应用、浏览器单页应用(SPA)、移动 App 或我们的 Agent 程序。将
client_secret
硬编码在这些环境中,极易被反编译或窃取。 - 认证关键: 为了解决无法使用
client_secret
的问题,引入了 PKCE(Proof Key for Code Exchange)机制。- 在第 5 步 " 发起授权请求 " 时,客户端发送一个公开的
code_challenge
(锁)。 - 在第 6 步 " 交换令牌 " 时,客户端发送一个保密的
code_verifier
(钥匙)。
- 在第 5 步 " 发起授权请求 " 时,客户端发送一个公开的
- 安全性:
code_verifier
扮演了一次性的、动态生成的client_secret
的角色。即使code
被截获,攻击者没有在客户端本地秘密保存的code_verifier
,同样无法换取令牌。
- 核心假设: 客户端是无法安全保管机密的环境,如桌面应用、浏览器单页应用(SPA)、移动 App 或我们的 Agent 程序。将