Agent 工程体系 · 第 37/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。
MCP 认证与授权:OAuth、客户端身份、Scope、令牌和代理风险
MCP 的认证与授权,解决的不是同一个问题:
- **认证(Authentication)**回答“你是谁”;
- **授权(Authorization)**回答“你能做什么”;
- **客户端身份(Client Identity)**回答“是哪一个 MCP 客户端发起了 OAuth 请求”;
- Scope回答“令牌被授予了哪些粗粒度权限”;
- **令牌(Token)**回答“调用方如何在后续请求中证明自己已经获得授权”;
- 对象权限回答“即使令牌拥有某个 Scope,它是否有权访问这一个租户、项目、文件或订单”。
如果把这些概念混为一谈,系统通常会出现两类错误:一类是“用户已经登录,所以什么都能访问”;另一类是“令牌有效,所以 MCP Server 可以把它转发给任意下游服务”。前者造成越权,后者造成令牌受众混淆和代理型安全漏洞。
本文以 2026-07-28 MCP 规范为基线,讨论 2026 年 9 月 Agent 工程中的 HTTP MCP 认证模型,同时说明 stdio、Streamable HTTP、OAuth 客户端注册、Scope 升级、令牌受众绑定和 MCP 代理的风险边界。MCP 授权规范本身只规定 HTTP-based transport 的授权流程;stdio 不应直接套用这套 OAuth 流程,而应通过环境等本地机制取得凭据。(modelcontextprotocol.io)
一、先建立完整的安全模型
1. MCP 中有哪些参与者
MCP 体系中至少存在以下参与者:
flowchart LR
U[Resource Owner<br/>用户或组织] --> C[Host / MCP Client]
C -->|OAuth 授权请求| AS[Authorization Server]
AS -->|Access Token| C
C -->|Bearer Token + MCP 请求| RS[MCP Server<br/>Resource Server]
RS -->|下游专用凭据| API[第三方 API]
其中:
- Resource Owner:资源所有者,通常是用户,也可能是组织或服务账户;
- Host:承载 Agent 的应用,例如 IDE、桌面客户端、企业 Agent 平台;
- MCP Client:Host 内部连接一个 MCP Server 的连接器;
- Authorization Server,AS:负责用户登录、同意和签发令牌;
- MCP Server:在 OAuth 术语中扮演 Resource Server,保护工具、资源或其他能力;
- 下游 API:MCP Server 可能访问的 Git、CRM、数据库、工单系统等外部服务。
MCP 规范中的 MCP Client 是 OAuth Client,受保护的 MCP Server 是 OAuth Resource Server,Authorization Server 负责与用户交互并签发供 MCP Server 使用的访问令牌。Authorization Server 可以与 MCP Server 部署在一起,也可以是独立服务。(modelcontextprotocol.io)
这里有一个容易忽略的事实:
MCP Client 不一定等于最终用户,也不一定等于 Agent。
一个 Host 可能有多个 MCP Client,分别连接不同的服务器。一个 MCP Client 又可能代表用户、代表企业租户,或者代表一个后台服务账户。因此,“客户端身份”和“用户身份”必须分开建模。
2. 认证与授权的逻辑关系
可以用一个授权判断函数描述 MCP Server 是否允许一次调用:
各变量含义如下:
Authenticated:令牌签发者、签名、有效期等验证通过;AudienceValid:令牌确实是签发给当前 MCP Server 的;ScopeSufficient:令牌的 Scope 覆盖本次操作;ObjectPolicySatisfied:调用者对目标对象有权限,例如只能访问当前租户;ActionPolicySatisfied:当前工具调用满足二次确认、审批、风险等级等业务条件。
这几个条件是“与”关系,而不是“或”关系。即使令牌有效,只要对象权限不满足,也必须拒绝。
例如:
用户 Alice
租户 tenant-a
令牌 Scope: files:read
目标对象: tenant-b/project-7/report.xlsx
操作: read
令牌认证成功,且 files:read 足够执行“读文件”这一类动作,但:
tenant-a != tenant-b
所以最终结果仍然应当是拒绝。
反例是只写如下代码:
if "files:read" in token.scopes:
return read_file(request.file_id)
这段逻辑只实现了 Scope 检查,没有检查租户、对象归属和用户对对象的关系。攻击者只需要把 file_id 替换成另一个租户的 ID,就可能读到不属于自己的数据。
二、MCP 授权覆盖哪些传输
1. stdio:本地进程边界,不是远程 OAuth 边界
stdio 是由客户端启动 MCP Server 子进程,双方通过标准输入和标准输出传递换行分隔的 JSON-RPC 消息。2026-07-28 规范将 stdio 定义为本地进程通信绑定;在这种传输下,MCP 授权规范建议不要使用 HTTP OAuth 流程,而应从环境中读取凭据。(modelcontextprotocol.io)
典型启动方式如下:
MCP_API_TOKEN='token-for-local-server' \
MCP_TENANT_ID='tenant-a' \
./my-mcp-server
MCP Server 可以从环境变量读取:
import os
api_token = os.environ["MCP_API_TOKEN"]
tenant_id = os.environ["MCP_TENANT_ID"]
这里的安全边界是:
Host 启动子进程
└── 环境变量和进程权限
└── stdio MCP Server
它没有经过浏览器,也没有 HTTP Authorization Header,更没有 MCP 规范定义的 OAuth Discovery 流程。
stdio 适合:
- 单机工具;
- IDE 插件;
- 由可信 Host 启动的文件系统或代码分析服务;
- 凭据已经由操作系统、密钥管理器或 Host 管理的场景。
但 stdio 并不意味着“天然安全”。如果 Host 会加载不可信配置,或者允许任意 MCP Server 命令,那么环境变量、文件路径、子进程权限和命令行参数都会变成攻击面。
尤其不要把长期高权限令牌直接拼在命令行参数中:
./server --token sk-production-secret
命令行参数可能出现在进程列表、诊断信息或崩溃报告中。环境变量也不是万能的,但通常比命令行参数更不容易被普通进程列表直接暴露。生产系统仍应优先使用操作系统密钥环、短期令牌或受限的本地凭据代理。
2. Streamable HTTP:令牌位于 HTTP 请求边界
Streamable HTTP 将每条 MCP 消息作为发往单一 MCP Endpoint 的 HTTP 请求,响应可以是 JSON,也可以是请求范围内的 SSE 流。2026-07-28 版本的传输层使用请求级模型,协议元数据位于消息体的 _meta 中,部分字段可以镜像到 HTTP Header,供网关路由和检查;消息体仍然是事实来源。(modelcontextprotocol.io)
授权令牌则必须放在 HTTP Header:
POST /mcp HTTP/1.1
Host: mcp.example.com
Content-Type: application/json
Authorization: Bearer eyJ...
MCP-Protocol-Version: 2026-07-28
MCP 客户端必须在发往 MCP Server 的每个 HTTP 请求中携带授权信息,访问令牌不得放入 URI 查询字符串。(modelcontextprotocol.io)
使用 curl 可以观察最小请求形态:
curl --fail-with-body \
-X POST 'https://mcp.example.com/mcp' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer eyJhbGciOi...' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/clientInfo": {
"name": "example-host",
"version": "1.0.0"
}
}
}
}'
这个请求能成立,需要同时满足:
- URL 指向真正的 MCP Endpoint;
- HTTPS 证书和域名验证通过;
- Bearer Token 未过期;
- Token 的签发者是 MCP Server 认可的 Authorization Server;
- Token 的受众包含当前 MCP Server;
- Token Scope 满足
tools/list或相关资源策略; - MCP Server 能正确解析 JSON-RPC 消息。
其中 clientInfo 是客户端自报信息,主要用于显示、日志和调试,不能被当作安全凭据。客户端在请求中声明自己叫 "example-host",并不代表服务端已经确认了这个身份。SDK 文档也明确将 clientInfo 和 serverInfo 视为 self-reported 信息,不应用于安全决策。(ts.sdk.modelcontextprotocol.io)
三、OAuth 流程不是“拿到一个 Token 就结束”
1. 第一步:从 MCP Server 发现 Authorization Server
MCP Server 必须提供 OAuth Protected Resource Metadata,用来声明其关联的 Authorization Server。客户端可以通过 401 Unauthorized 响应中的 WWW-Authenticate Header 发现元数据地址,也可以访问规定的 .well-known/oauth-protected-resource 地址。(modelcontextprotocol.io)
例如,MCP Server 返回:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
客户端应当先请求:
curl --fail \
'https://mcp.example.com/.well-known/oauth-protected-resource'
可能得到:
{
"resource": "https://mcp.example.com",
"authorization_servers": [
"https://auth.example.com"
],
"scopes_supported": [
"files:read",
"files:write"
]
}
这里有两个不同的字段:
authorization_servers:哪些 Authorization Server 可以给该 MCP Server 签发令牌;scopes_supported:该资源服务器声明支持的 Scope 集合。
scopes_supported 不一定包含运行时所有动态 Scope,也不一定等同于某一次 401 或 403 Challenge 中要求的 Scope。运行时 Challenge 对当前操作更具针对性,客户端不能假设二者存在固定的子集或超集关系。(modelcontextprotocol.io)
客户端随后请求 Authorization Server Metadata。对于带路径的 issuer,例如:
https://auth.example.com/tenant-a
客户端需要按规范尝试多个 well-known 地址,并校验元数据中的 issuer 是否与实际使用的 issuer 标识完全一致。若从攻击者地址获取到的文档声称 issuer 是另一个地址,客户端必须拒绝使用该元数据。(modelcontextprotocol.io)
2. 第二步:确定客户端身份
OAuth Client ID 表示“哪个应用发起授权”。它不等于用户 ID,也不等于 Access Token。
MCP 支持三类客户端注册方式:
- 预注册:服务端和客户端提前建立关系;
- Client ID Metadata Document,CIMD:客户端使用一个 HTTPS URL 作为
client_id,该 URL 指向客户端元数据文档; - Dynamic Client Registration,DCR:客户端向 Authorization Server 的注册端点动态注册。
2026-07-28 基线中,CIMD 是无预先关系场景下的首选方式;DCR 已弃用,但为兼容旧 Authorization Server 仍可使用。客户端若支持多种方式,应优先使用预注册,然后使用 CIMD,再以 DCR 作为回退。(modelcontextprotocol.io)
一个 CIMD 文档可以是:
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"client_uri": "https://app.example.com",
"redirect_uris": [
"http://127.0.0.1:3000/callback",
"http://localhost:3000/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
要求重点在于:
client_id必须是 HTTPS URL;- URL 必须有路径,不能只是站点根地址;
- 文档中的
client_id必须与文档 URL 完全匹配; - 必须声明
client_name和redirect_uris; - Authorization Server 必须校验授权请求中的 Redirect URI 是否与文档内容匹配。(modelcontextprotocol.io)
CIMD 的关键变化是:客户端不再对每一个陌生 Authorization Server 发起一次注册请求,而是发布一份可被 Authorization Server 获取的客户端元数据。这样更适合 MCP 的“客户端和服务器经常没有预先关系”的生态。
但 CIMD 不等于签名证明。Authorization Server 获取一个 HTTPS JSON 文档,并不自动证明请求者控制了这个客户端。服务端仍需实施域名信任策略、文档缓存策略、SSRF 防护和 Redirect URI 校验。
3. 第三步:使用 Authorization Code 和 PKCE
典型的授权码流程包含:
sequenceDiagram
participant C as MCP Client
participant M as MCP Server
participant AS as Authorization Server
participant U as User
C->>M: 请求 MCP 资源,无 Token
M-->>C: 401 + resource_metadata + scope
C->>M: 获取 Protected Resource Metadata
M-->>C: authorization_servers + scopes_supported
C->>AS: 获取 Authorization Server Metadata
AS-->>C: authorize/token endpoint + PKCE 能力
C->>AS: authorization request + state + code_challenge + resource + scope
AS->>U: 登录与授权确认
U-->>AS: 同意
AS-->>C: code + state + iss
C->>AS: token request + code_verifier + resource
AS-->>C: access_token + 可选 refresh_token
C->>M: Authorization: Bearer access_token
M-->>C: MCP 响应
PKCE 是授权码保护机制。客户端生成:
code_verifier:只有客户端知道的随机值;code_challenge:由code_verifier计算得到,通常使用S256。
授权请求发送 code_challenge,换取 Token 时发送 code_verifier。Authorization Server 只有在二者匹配时才兑换授权码。
直觉上,授权码可能被攻击者截获,但攻击者没有 code_verifier,所以不能把它兑换成令牌。MCP 客户端必须实现 PKCE,并在确认 Authorization Server 支持 PKCE 后才继续;技术上可行时必须使用 S256。(modelcontextprotocol.io)
一个简化的参数示例:
GET /authorize?
response_type=code
&client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient-metadata.json
&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback
&scope=files%3Aread
&resource=https%3A%2F%2Fmcp.example.com
&state=random-state
&code_challenge=base64url-sha256(verifier)
&code_challenge_method=S256
其中 resource 很重要。MCP 客户端必须在授权请求和 Token 请求中都带上 resource,并指明要访问的 MCP Server 的规范 URI。(modelcontextprotocol.io)
四、客户端身份不等于令牌受众
这是 MCP OAuth 中最容易被错误实现的一组关系。
设:
client_id = client-A
issuer = https://auth.example.com
resource = https://mcp.example.com
subject = user-123
scope = files:read
它们表达的是不同维度:
| 字段 | 说明 |
|---|---|
client_id |
哪个 OAuth 客户端发起授权 |
issuer |
哪个 Authorization Server 签发令牌 |
subject |
令牌代表哪个用户或主体 |
resource / aud |
令牌准备给哪个资源服务器使用 |
scope |
令牌获得哪些粗粒度权限 |
| 业务声明 | 租户、组、项目、对象范围等 |
一个“有效”的令牌至少要满足:
其中:
SignatureValid:签名和密钥验证成功;IssuerTrusted:签发者是该 MCP Server 配置或发现得到的可信 issuer;NotExpired:未超过有效期;Audience = MCPServer:令牌的受众是当前 MCP Server;Scope ⊇ RequiredScope:权限集合覆盖本次操作需求。
MCP Server 必须验证令牌是专门签发给自己的,不能因为令牌是同一身份系统签发的,就接受它访问其他资源。MCP Client 也不得把一个 Authorization Server 签发的凭据随意发送给另一个 Authorization Server 管理的资源。(modelcontextprotocol.io)
因此,下面这种做法是错误的:
# 错误:只验证签名和 issuer
claims = verify_jwt(token, issuer="https://auth.example.com")
if claims["scope"].find("files:read") >= 0:
return read_file()
正确逻辑至少应包含受众和对象授权:
claims = verify_jwt(
token,
issuer="https://auth.example.com",
audience="https://mcp.example.com"
)
require_scope(claims, "files:read")
require_tenant_access(claims, requested_tenant_id)
require_object_access(claims["sub"], requested_file_id)
实际系统中,JWT 验证库的参数名称会因语言和库而不同,但验证维度不能省略。
五、Scope 是粗粒度能力,不是完整对象权限
1. Scope 的含义
Scope 是 OAuth 令牌中的权限字符串集合,例如:
files:read files:write
它通常表达:
- 是否可以读文件;
- 是否可以写文件;
- 是否可以创建工单;
- 是否可以执行某类工具。
Scope 适合表达“能力类型”,不适合独立表达完整的业务权限。
例如:
files:read
最多表示调用者具备某类文件读取能力,但不自然地表达:
只能读取 tenant-a;
只能读取 project-7;
只能读取用户本人创建的文件;
不能读取标记为 confidential 的文件;
需要用户在本次操作中再次确认。
所以,完整授权通常分层进行:
OAuth Scope
↓
主体与租户关系
↓
对象归属和对象级 ACL
↓
操作风险与二次确认
2. 初始 Scope 选择
MCP Server 可以在 401 的 WWW-Authenticate Header 中给出当前资源需要的 Scope:
WWW-Authenticate: Bearer
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"
客户端选择初始 Scope 时,应优先使用 Challenge 中的 Scope;如果没有 Challenge Scope,再参考 Protected Resource Metadata 中的 scopes_supported。客户端不应一开始就请求所有高权限 Scope,因为这会扩大用户授权面,并增加令牌泄漏后的影响范围。(modelcontextprotocol.io)
可以把初始权限集合记为:
若不存在 Challenge Scope,则:
但这只是起始集合,不代表未来所有操作都能完成。
3. 运行时 Scope Challenge 和 Step-Up
假设客户端已有:
Token Scope = files:read
此时 Agent 调用了删除工具:
{
"method": "tools/call",
"params": {
"name": "delete_file",
"arguments": {
"file_id": "f-100"
}
}
}
MCP Server 发现该操作需要 files:write,应返回类似:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer
error="insufficient_scope",
scope="files:write",
resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
error_description="File write permission required for this operation"
客户端不能简单地把旧 Scope 替换掉。正确的 Scope 集合是并集:
在这个例子中:
S_old = { files:read }
S_challenge = { files:write }
S_new = { files:read, files:write }
如果客户端只请求 files:write,新令牌可能不再包含 files:read,导致之前可用的操作突然失败。MCP 规范要求客户端在 Step-Up 时累计旧权限与当前 Challenge 权限,并限制重新授权和重试次数,避免失败循环。(modelcontextprotocol.io)
服务端也应一次性返回当前操作所需的全部 Scope:
错误做法:
第一次返回 files:write
第二次返回 files:delete
第三次返回 files:admin
正确做法:
一次返回 files:write files:delete
否则一次操作会被拆成多轮授权,用户体验更差,也增加授权状态机出错的概率。
六、访问令牌、刷新令牌和令牌存储
1. Access Token
Access Token 是客户端访问 MCP Server 时使用的凭据。对于 HTTP MCP,形式是:
Authorization: Bearer <access-token>
它通常具有较短有效期,并携带 Scope、受众、主体、签发时间等信息。MCP Server 必须在处理 MCP 请求前完成令牌验证;无效或过期令牌应返回 401。无效 Scope 或权限不足通常返回 403。(modelcontextprotocol.io)
不要把 Access Token 放在:
URL 查询参数
Referer
普通业务日志
MCP 工具参数
模型上下文
错误消息
错误示例:
https://mcp.example.com/mcp?access_token=eyJ...
查询字符串可能进入代理访问日志、浏览器历史、链路追踪、监控标签和缓存键。MCP 规范明确禁止将 Access Token 放入 URI 查询字符串。(modelcontextprotocol.io)
2. Refresh Token
Refresh Token 用于在 Access Token 过期后获取新令牌。它的权限和生命周期通常比 Access Token 更敏感,因为持有者可能长期维持用户授权。
MCP 客户端如果需要 Refresh Token:
- 必须安全存储和传输;
- 可以在客户端元数据中声明支持
refresh_tokengrant; - 不能假设 Authorization Server 一定会签发;
- 对公开客户端,应使用刷新令牌轮换。(modelcontextprotocol.io)
一个合理的存储分层是:
Access Token -> 内存或短期加密缓存
Refresh Token -> 操作系统密钥环 / KMS / 加密数据库
客户端密钥 -> 仅限机密客户端,不能放入前端
对于桌面应用、CLI 或本地 Agent,通常属于 public client,不能依赖“客户端秘密不泄漏”来保护授权流程。因为安装包中的 secret 最终可以被用户或攻击者提取,真正的保护应依靠 PKCE、精确 Redirect URI、短期 Access Token 和刷新令牌轮换。
七、授权响应中的 iss:防止 Authorization Server Mix-Up
1. 什么是 Mix-Up
假设客户端同时连接两个 MCP Server:
MCP-A -> AS-A
MCP-B -> AS-B
攻击者控制 AS-B,并诱导客户端把本应发送给 AS-A 的授权响应交给 AS-B。若客户端只根据 state 或 code 处理响应,而不检查授权响应来自哪个 issuer,就可能把授权码提交到错误的 Token Endpoint。
这就是 Authorization Server Mix-Up。
2. MCP 的校验方式
客户端在开始授权时,应记录经过验证的 issuer:
authorization_request_id = r-001
expected_issuer = https://auth.example.com/tenant-a
state = random-state
code_verifier = random-verifier
回调收到:
code=abc
state=random-state
iss=https://auth.example.com/tenant-a
客户端需要:
- 校验
state; - 将回调中的
iss与之前记录的 issuer 做字符串比较; - 不进行大小写、默认端口、尾部斜杠或百分号编码等“宽松规范化”;
- 校验通过后,才把
code发给 Token Endpoint。
2026-07-28 规范要求客户端按照 RFC 9207 规则验证 iss。如果 Authorization Server 元数据声明支持 iss,但响应缺少 iss,客户端必须拒绝;如果响应带有 iss,客户端也必须与已记录 issuer 比较。(modelcontextprotocol.io)
反例:
# 错误:只要 state 正确就使用 code
if callback["state"] == saved_state:
exchange_code(callback["code"])
更安全的逻辑是:
if callback["state"] != saved_state:
raise AuthorizationError("state mismatch")
if callback.get("iss") != expected_issuer:
raise AuthorizationError("issuer mismatch")
exchange_code(
code=callback["code"],
code_verifier=saved_code_verifier,
resource="https://mcp.example.com"
)
如果服务端尚未提供 iss,客户端应依据其元数据中的 authorization_response_iss_parameter_supported 决定是否接受缺失值;生产实现不应为了兼容而无条件关闭 issuer 校验。
八、MCP 代理最危险的问题:Token Passthrough
1. 代理拓扑
考虑如下架构:
MCP Client
|
| Token T_client-for-proxy
v
MCP Proxy
|
| Downstream API
v
Third-party API
客户端给 Proxy 的令牌,是为 Proxy 签发的:
aud(T_client) = MCP Proxy
Proxy 再访问下游 API 时,应该取得一个新的、面向下游 API 的令牌:
aud(T_downstream) = Third-party API
正确的数据流是:
T_client-for-proxy
--验证-->
Proxy 内部身份与授权
--重新授权或凭据交换-->
T_downstream-for-api
错误的数据流是:
T_client-for-proxy
--原样转发-->
Third-party API
这叫 Token Passthrough,即令牌透传。MCP 规范明确禁止 MCP Server 将收到的客户端令牌原样传递给上游服务;上游调用应使用由上游 Authorization Server 签发的独立令牌。(modelcontextprotocol.io)
2. 为什么令牌透传危险
令牌的安全属性包括:
issuer
audience
subject
scope
expiry
Proxy 收到的令牌通常满足:
audience = Proxy
如果下游 API 接受它,实际上就意味着:
下游 API 信任一个并非为自己签发的令牌
这会造成至少三个问题:
- 受众混淆:同一令牌被多个资源服务器接受;
- 权限扩大:Proxy 侧的 Scope 语义可能被误解释为下游权限;
- 信任边界泄漏:下游 API 被迫理解上游身份系统和 MCP 客户端的令牌格式。
正确的代理授权应当明确区分:
用户是否可以调用 Proxy 的工具
Proxy 是否可以访问下游 API
用户对下游对象是否有权限
这三个问题需要分别判断。
九、Confused Deputy:代理有权限,不代表用户有权限
Confused Deputy 可以理解为“有权限的代理被低权限调用者诱导,替调用者执行了本不该执行的动作”。
例如:
MCP Proxy 拥有企业级 GitHub Token
用户 Bob 只能访问仓库 A
Agent 被诱导调用 Proxy 的 read_repository(repo=B)
Proxy 使用自己的企业 Token 读取仓库 B
如果 Proxy 只验证:
Proxy 自己能不能访问 repo=B
而没有验证:
Bob 是否能访问 repo=B
那么 Proxy 就成为了权限放大器。
形式化地说,代理调用下游 API 的允许条件应是:
不能简化为:
MCP 规范将“作为第三方 API 中介的 MCP Server”列为 Confused Deputy 风险场景,并要求使用静态客户端身份的代理,在向第三方 Authorization Server 转发动态注册客户端之前取得用户同意。(modelcontextprotocol.io)
工程上,代理至少要保留以下上下文:
{
"actor": "user-123",
"tenant": "tenant-a",
"client_id": "host-client-1",
"mcp_server": "https://proxy.example.com",
"tool": "read_repository",
"object": {
"type": "repository",
"id": "repo-b"
},
"scopes": ["repo:read"]
}
然后在真正访问下游对象前执行对象级授权,而不是仅仅把 actor 写进日志。
十、代理、网关和 WAF 的 Header 风险
2026-07-28 Streamable HTTP 支持将部分 MCP 元数据镜像到 HTTP Header,使网关可以按方法和工具路由、限流或审计;但消息体仍是事实来源,Header 与 Body 不一致时应拒绝请求。(modelcontextprotocol.io)
例如:
Mcp-Method: tools/call
Mcp-Name: delete_file
消息体中却是:
{
"method": "tools/call",
"params": {
"name": "read_file"
}
}
如果网关按 Header 判断“这是一个低风险读操作”,而后端按 Body 执行删除操作,就会产生授权绕过。
正确的处理顺序是:
1. 网关解析并规范化 Header
2. 后端解析 Body
3. 比较 Header 与 Body
4. 不一致则拒绝
5. 仅在一致后执行路由、审计和授权
类似风险还包括:
- 代理删除
AuthorizationHeader,导致客户端不断重复授权; - 代理把用户 Token 写入下游请求;
- 网关缓存带有用户数据的 MCP 响应;
- WAF 记录完整 Bearer Token;
- 反向代理修改 Host、Path 或 Scheme,导致 Resource URI 不一致;
- 多租户网关按
Host路由,但 Token 的aud仍指向另一个租户服务; - 重试中间件在令牌过期后自动重放带副作用的工具调用。
对于带副作用的工具,例如:
delete_file
send_email
create_payment
rotate_secret
HTTP 重试不能只依据“网络错误”。必须结合请求是否已经到达后端、是否具有幂等键、是否完成用户确认和是否产生业务结果进行判断。
十一、CIMD 的 SSRF 与 localhost 风险
CIMD 要求 Authorization Server 获取客户端提供的 HTTPS 元数据 URL。这会把“服务端主动发起 HTTP 请求”引入 OAuth 注册流程,因此带来 SSRF 风险。
攻击者可能提交:
https://attacker.example/client.json
文档内容再诱导 Authorization Server 访问:
http://169.254.169.254/latest/meta-data/
或者访问内部管理端口。
Authorization Server 获取 CIMD 时应考虑:
- 禁止访问云实例元数据地址;
- 限制内网 IP、回环地址和链路本地地址;
- 避免无限重定向;
- 限制响应大小和超时;
- 校验 Content-Type 与 JSON 结构;
- 对域名解析结果重新检查;
- 按组织策略限制允许的客户端域名;
- 缓存时尊重 HTTP Cache Header,但不能永久信任远程文档。
规范明确要求 Authorization Server 处理 CIMD 时考虑 SSRF 防护。(modelcontextprotocol.io)
另一个风险是 localhost Redirect URI:
http://localhost:3000/callback
本地应用常用这种回调地址,但“localhost”并不证明回调一定属于原始客户端。另一个本地进程可能抢占端口、监听回调,或者诱导用户向错误应用授权。
因此 Authorization Server 至少应:
- 在授权页明确显示 Redirect URI 的主机名;
- 对仅使用 localhost 的客户端显示额外提醒;
- 精确校验完整 Redirect URI;
- 不允许任意路径和任意端口的模糊匹配。
MCP 规范要求 Redirect URI 使用 HTTPS 或 localhost,并要求授权服务器对已注册 URI 进行精确校验。(modelcontextprotocol.io)
十二、错误状态码与诊断路径
1. 401 Unauthorized
401 通常表示:
- 没有发送令牌;
- 令牌格式错误;
- 令牌过期;
- 签发者不可信;
- 受众不匹配;
- 令牌无法验证。
服务端应通过 WWW-Authenticate 提供资源元数据地址,必要时给出初始 Scope。
诊断示例:
curl -i -X POST 'https://mcp.example.com/mcp' \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
预期应看到:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="..."
如果直接返回 HTML 登录页,客户端可能无法完成 MCP OAuth Discovery。
2. 403 Forbidden
403 通常表示令牌已经被识别,但权限不足:
- Scope 不足;
- 用户没有目标租户权限;
- 用户没有目标对象权限;
- 工具需要二次确认;
- 组织策略禁止当前操作。
运行时 Scope 不足时,应返回:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer
error="insufficient_scope",
scope="files:write",
resource_metadata="..."
不要把所有授权错误都返回 401。如果客户端已经有有效令牌,却因为 Scope 不足得到 401,客户端可能错误地重新执行完整登录,而不是进行增量授权。
3. 常见故障定位表
| 表现 | 常见原因 | 首先检查 |
|---|---|---|
| 每次请求都触发登录 | Token 没被代理转发 | Authorization Header 是否到达 MCP Server |
| Token 有效但总是 403 | Scope、租户或对象权限不足 | scope、tenant_id、对象 ACL |
| Token 在 MCP Server 有效,在下游失败 | 错误地透传了 MCP Token | 下游 API 的 aud 是否匹配 |
| 授权回调报 issuer mismatch | 多 Authorization Server 混用或代理改写 | 记录的 issuer、回调 iss、元数据 issuer |
| localhost 回调偶发失败 | 端口冲突或本地进程拦截 | Redirect URI、监听端口、进程所有者 |
| 注册请求超时 | CIMD 获取失败或被安全网关拦截 | 客户端元数据 URL、DNS、SSRF 防护 |
| Agent 重复执行删除 | HTTP 自动重试副作用请求 | 幂等键、响应确认、重试策略 |
| 网关审计显示 read,后端执行 write | Header 与 Body 不一致 | Mcp-Method、Mcp-Name 与 JSON-RPC 内容 |
生产日志中可以记录:
issuer
client_id
subject hash
resource
audience
scope hash
tenant_id
tool name
object type/id hash
authorization result
不应记录:
完整 Access Token
完整 Refresh Token
授权码
PKCE code_verifier
包含敏感数据的工具参数
十三、一个端到端的最小实现检查
一个支持 HTTP OAuth 的 MCP Client,在第一次请求时可以按如下顺序工作:
1. 向 MCP Endpoint 发起请求
2. 收到 401
3. 解析 WWW-Authenticate
4. 获取 Protected Resource Metadata
5. 选择 Authorization Server
6. 获取并验证 Authorization Server Metadata
7. 确定客户端注册方式
8. 验证 PKCE 支持
9. 生成 state 和 code_verifier
10. 携带 resource 和最小 Scope 发起授权
11. 校验回调 state 和 iss
12. 使用 code_verifier 兑换 Token
13. 以 Authorization Header 调用 MCP
14. 收到 403 insufficient_scope 时计算 Scope 并集
15. 重新授权并限制重试次数
16. 令牌刷新失败时清除无效凭据并重新开始 Discovery
MCP Server 的对应检查顺序是:
1. 读取 Authorization Header
2. 验证 Bearer 格式
3. 验证 issuer、签名和有效期
4. 验证 audience/resource
5. 提取 subject、租户和 Scope
6. 根据工具和参数计算所需 Scope
7. 执行租户和对象权限检查
8. 执行二次确认或审批策略
9. 如访问下游,使用下游专用令牌
10. 返回 MCP 响应或标准 HTTP 授权错误
注意第 8 步不能被 Scope 替代。files:write 可以表示“用户具备写文件能力”,但不一定表示“删除生产环境文件无需二次确认”。
十四、规范保证、常见实现和工程建议
规范保证
在 2026-07-28 基线下,以下属于 MCP 授权规范中的关键要求:
- HTTP 授权流程基于 OAuth 2.1 相关机制;
- MCP Server 必须提供 Protected Resource Metadata;
- 客户端必须支持 Authorization Server Discovery;
- 客户端必须在授权请求和 Token 请求中携带
resource; - Access Token 必须通过
Authorization: BearerHeader 发送; - Access Token 不得出现在 URI 查询字符串;
- MCP Server 必须验证令牌受众;
- MCP Server 不得透传收到的客户端令牌;
- 客户端必须使用 PKCE 并验证其支持;
- 客户端必须根据规则校验授权响应中的
iss; - DCR 已弃用,CIMD 是新实现的优先方向。(modelcontextprotocol.io)
常见实现
以下能力常见于 SDK 或 OAuth 基础设施,但不应直接当作协议保证:
- JWT 格式的 Access Token;
- JWKS 远程公钥验证;
- 自动刷新 Access Token;
- 自动处理
insufficient_scope; - 浏览器回调服务器;
- Redis 或数据库中的 Token 缓存;
- API Gateway 统一校验令牌;
- OIDC 作为 Authorization Server 的身份层。
MCP 官方 SDK 页面显示,TypeScript、Python、C#、Go、Rust 等 SDK 属于 Tier 1,SDK 普遍支持本地和远程传输;具体 OAuth API、版本和默认行为仍应以所用 SDK 的版本文档为准。(modelcontextprotocol.io)
工程建议
工程上可以采用以下边界:
OAuth:
负责主体认证、客户端注册、粗粒度能力
MCP Server:
负责 Token 验证、受众验证、Scope 检查
业务授权层:
负责租户、项目、对象、关系和状态检查
风险控制层:
负责二次确认、审批、幂等、审计和撤销
下游连接器:
负责获取并使用下游专用令牌
这比把所有逻辑压缩到一段 verify_token() 中更容易审计,也更容易定位失败原因。
结语:MCP 授权的最小正确答案
一个 MCP 请求是否安全,不能只问:
Token 有效吗?
至少要连续回答:
1. 令牌是谁签发的?
2. 令牌是给谁使用的?
3. 客户端身份是什么?
4. 令牌代表哪个主体?
5. 主体属于哪个租户?
6. Scope 是否覆盖当前动作?
7. 主体是否有权访问当前对象?
8. 当前工具是否需要二次确认?
9. MCP Server 是否错误透传了令牌?
10. 代理是否把自己的权限误当成用户权限?
OAuth 解决的是跨网络的授权委托,Client ID 解决的是客户端识别,Scope 解决的是能力分类,Token 解决的是请求证明;它们都不能单独完成对象级授权。
在 Agent 系统中,真正的安全边界应当是:
只有把身份、受众、Scope、租户、对象和代理上下文同时保留下来,MCP 的认证与授权才不会从“登录流程”退化成一个看似有效、实际缺少权限边界的 Bearer Token 转发系统。
系列导航与关联阅读
- 系列入口:Agent 工程完整路线:从运行循环、记忆与协议到安全、评测和生产交付
- 上一篇:MCP 传输:stdio、Streamable HTTP、会话、重连和代理
- 下一篇:MCP Server 工程:能力注册、Context、并发、错误和部署
- 延伸:Agent 认证与授权:Actor、租户、对象权限、Scope 和二次校验
官方资料
本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。

评论
0 条讨论