你将 Claude Code 连接到 MCP 服务器。服务器需要认证。你的客户端发送请求;服务器回复带有 401 Unauthorized 和 WWW-Authenticate: Bearer 头的响应,其中包含一个 resource_metadata URL。接下来发生的就是一个由三个 RFC 和一组 MCP 特定扩展定义的 OAuth 2.1 流程,这些 RFC 和扩展共同解决了代理安全中较难的问题之一:确保获取的用于与此MCP服务器通信的令牌不能被重放到那个服务器。
这篇文章追踪了完整的流程——规范中说明了什么、RFC 实际提供了什么,以及实际中安全陷阱隐藏在哪里。
流程:从 401 到资源绑定令牌
MCP 授权模型(修订版 2025-11-25,未更改地延续到发布候选版——冻结版 2026-05-21——针对计划于 2026-07-28 发布的最终规范)是一个两阶段的过程。阶段 1 是检测:承载 WWW-Authenticate: Bearer resource_metadata="..." 的 401 告诉客户端该服务器受 OAuth 保护,以及在哪里可以找到它的元数据。阶段 2 是授权访问:使用元数据,发现授权服务器,获取绑定到此特定服务器的令牌,并使用它。
具体步骤:
-
401 + WWW-Authenticate:MCP 服务器拒绝未认证的请求。挑战中的
resource_metadata参数指向服务器的受保护资源元数据文档。 -
PRM 获取 (RFC 9728):客户端 GET
/.well-known/oauth-protected-resourceURL。响应是一个 JSON 文档,声明服务器的规范资源 URI、保护它的授权服务器以及资源支持的范围。客户端验证文档的resource字段是否与其预期访问的服务器匹配——不匹配是冒充信号,必须拒绝。 -
AS 发现 (RFC 8414):客户端使用 PRM 的
authorization_servers数组中的发行者 URL,遍历已知候选项(/.well-known/oauth-authorization-server,然后/.well-known/openid-configuration)来获取授权服务器元数据。返回文档中的发行者必须与获取它时的发行者字节完全相同——不是“规范化后等价”,也不是“差不多”。字节完全相同。 -
令牌获取 (RFC 8707):客户端从 AS 的令牌端点请求令牌,同时在授权请求和令牌请求中都包含
resource=<canonical server URI>。这将令牌的受众绑定到特定的 MCP 服务器。AS 会颁发一个其受众为该资源的令牌,客户端将其提供给服务器。 -
授权访问:客户端使用令牌调用 MCP 服务器的只读自省方法(
tools/list、resources/list等)。令牌证明客户端已获得授权;资源指示器证明令牌是为此服务器而生成的。
RFC 9728 实际提供的内容
RFC 9728(受保护资源元数据)是一种发现机制。它回答:“哪个授权服务器保护此资源,以及它期望什么?”它不会对服务器进行身份验证,也不会验证令牌或执行访问控制。这些是独立的事项。
PRM 文档有一个必填字段(resource)和一组可选字段,其中 authorization_servers 最为重要。MCP 规范收紧了 RFC 的可选性:至少需要一个授权服务器。列出零个授权服务器的 PRM 文档被视为错误。
resource 字段是受保护资源的规范 URI。客户端必须将其与其打算联系的服务器进行比较,并拒绝不匹配的情况:
// RFC 9728 §3.3:PRM 的资源值必须识别客户端正在访问的受保护
// 资源——与客户端将其令牌绑定到的资源指示符进行简单字符串比较。
// 这一检查防止了一类攻击,即恶意或配置错误的 PRM 文档声称代表不同的资源。这种比较不进行规范化——它是与客户端为所给的服务器 URL 计算的规范资源 URI 进行直接字符串匹配。
if prm.Resource != c.resource {
return authServerMetadata{}, fmt.Errorf(
"mcp: oauth: protected resource metadata declares resource %q, "+
"expected %q (RFC 9728 §3.3 reject)", prm.Resource, c.resource)
}
这一检查可防止一类攻击:恶意或配置错误的 PRM 文档声称代表另一个资源。比较过程不会做规范化,而是将客户端根据给定服务器 URL 计算出的规范资源 URI 直接进行字符串匹配。
RFC 8707 资源指示符——令牌绑定为何重要
如果没有资源指示器,从 AS 获取的访问令牌可能会在 AS 保护的任何资源服务器上使用。如果你有两个 MCP 服务器——比如,一个只读文档服务器和一个代码执行沙箱——都在同一个身份提供者之后,为其中一个获取的令牌可能会被提交给另一个。这就是“混淆代理”问题。
RFC 8707 通过在授权和令牌请求中添加 resource 参数来解决这个问题。AS 发放的令牌,其受众明确为该资源 URI。正确验证的资源服务器会拒绝受众与自身身份不匹配的令牌。
在实际操作中,令牌请求会在授权类型旁边包含资源指示器:
form := url.Values{
"grant_type": {"client_credentials"},
"resource": {c.resource}, // RFC 8707 — 将令牌绑定到受众
}
这出现在连接器使用的每种授权类型中:客户端凭证、授权码兑换和刷新令牌轮换。资源指示符不是可选的——它存在于每个令牌请求中,因此每个令牌从构造上都是面向受众的。
字节完全相同的发行者检查
整个流程中最关键的安全验证也是最容易描述和最容易出错的:AS 元数据文档中的发行者值必须与客户端用于构建知名 URL 的发行者字节完全相同。
不是大小写不敏感的相等。不是进行方案规范化后的等效。去掉尾部斜杠后也不是一样。必须字节完全相同。RFC 8414 第 3.3 节对此有明确说明,MCP 规范继承了这一要求。
// discoverASMetadata:拒绝 issuer 与获取目标不一致的文档;
// 根据 RFC 8414 §3.3,issuer 必须逐字节完全相同。
// 不匹配的文档是身份冒充信号。
if as.Issuer != issuer {
return authServerMetadata{}, fmt.Errorf(
"mcp: oauth: AS metadata at %s declares issuer %q, "+
"expected %q (RFC 8414 §3.3 reject)", cand, as.Issuer, issuer)
}
为什么这么严格?因为一个控制 DNS 或位于网络路径上的攻击者可以提供一个元数据文档,该文档指向他们自己的令牌端点,同时声称自己是合法的发行者。如果客户端在比较之前对发行者进行规范化,https://auth.example.com 和 https://AUTH.example.com 将匹配——攻击者的文档就会被接受。字节完全相同的比较解决了这个问题。
相同的规则适用于 RFC 9207(授权响应发布者验证)。当客户端启动授权码流程时,它会从已验证的 AS 元数据中记录发布者。在重定向回来的响应中,iss 参数必须与记录的值完全匹配——同样是逐字节匹配,不进行任何规范化——在授权码被兑换之前。这就是防混淆机制:如果没有这个机制,一个恶意的 AS 可能会拦截该码,并让客户端在攻击者的令牌端点兑换它。
客户端标识:CIMD 取代 DCR
MCP 规范定义了客户端向授权服务器标识自己的优先顺序:
- 预注册凭据 — 运营者提前提供
client_id和client_secret - CIMD(客户端 ID 元数据文档) — 客户端在 HTTPS URL 上托管一个 JSON 文档;该 URL 就是
client_id - 动态客户端注册(RFC 7591) — 客户端在 AS 的注册端点注册自身
- 提示用户 — 不适用于无界面代理
DCR 在计划于 2026-07-28 发布的最终规范候选版本中已被弃用,取而代之的是 CIMD。原因是操作性的:DCR 会在授权服务器上创建持久的客户端状态。每个注册的代理都会留下一个 client_id/client_secret 对,AS 必须存储它们,但没有人跟踪或撤销它们。对于一大批代理而言,这是无法控制的凭证扩散。
CIMD 颠倒了模型。客户端在它控制的 URL 上托管一个文档。AS 在需要验证客户端时获取该文档,根据 HTTP 缓存头进行缓存,并且不会永久存储任何内容。密钥轮换即文档更新。停用客户端即删除文档。AS 不会积累孤立的注册信息。
权衡:CIMD 需要客户端运行一个 HTTPS 端点。对于自托管的治理平面来说,这是自然的——该平面已经运行 HTTPS 服务。对于开发者笔记本上的 CLI 工具,则不太适合,这也是为什么预注册的凭证在优先顺序中仍然是首选。
CIMD 身份不能携带共享密钥(文档 URL 是公开的——其中的任何秘密都将成为凭证泄露)。客户端认证使用 private_key_jwt(RFC 7523):客户端使用私钥对短期效验的 JWT 进行签名,其公钥对应物发布在 CIMD 文档的 jwks 字段中。
“绝不透传”规则
一个容易被忽视的结构性防御措施:连接器只使用它自己获取的令牌,针对特定服务器,通过上述发现流程获得。它从不接受第三方的令牌,也不会将其转发到 MCP 服务器。它从不读取传入请求的令牌并进行转发。
这是协议级别的“混乱副官”防御。如果客户端转发它收到的令牌,攻击者可能会提交一个针对低权限资源的令牌,让客户端将其转发到高权限资源(或反之——通过欺骗客户端向攻击者控制的服务器提交,高权限令牌可能被提取)。通过获取自己的令牌并且从不接触他人的令牌,连接器无法被用作令牌中继。
令牌直通不仅不鼓励——从结构上来说是不可能的。连接器用于 OAuth 流程的 HTTP 客户端与任何入站请求处理程序是分开的。没有任何代码路径会从传入请求中读取承载令牌并写入到传出请求中。
SSRF:DNS 重绑定防护
连接器获取的每个元数据和令牌端点 URL 在两个层面上都受到 SSRF 保护。第一个是预检检查:URL 必须是 HTTPS(本地开发的回环除外),且不能是字面上的保留 IP 地址。
第二个是拨号时检查,用于关闭 DNS 重新绑定的 TOCTOU。在预检期间解析为公共 IP 的主机名,到套接字拨号时可能会重新绑定到私有 IP。连接器的 HTTP 客户端安装了一个 net.Dialer.Control 函数,该函数在连接时检查具体解析的 IP,并拒绝任何保留地址。这是权威检查——预检是对明显情况的快速拒绝,但拨号时检查才是关键。
范围提升
MCP 服务器可以用 WWW-Authenticate: Bearer error="insufficient_scope" scope="mcp:tools:list mcp:resources:read" 回应请求。该规范 (SEP-835/SEP-2350) 定义了客户端如何处理此情况:计算之前请求的作用域与服务器刚刚挑战的作用域的并集,然后使用这个扩展的集合重新获取令牌。并集保留了之前授予的权限并添加新的权限。提升操作只发生一次——同一请求上的第二次作用域不足挑战不会重试,以防止无限循环。
服务器在其挑战中可以是无状态的:它仅命名当前操作需要的作用域,而不是客户端之前可能请求的完整集合。客户端累积处理使得无需服务器跟踪每个客户端的作用域历史也能实现此功能。
实际上这意味着
OAuth 流程对于 MCP 服务器来说有明确定义,并且在严格实施时,可以应对真实的攻击面:跨服务器的令牌重放、元数据冒充、DNS 重新绑定 SSRF 以及无法控制的客户端凭证扩散。字节级相同的颁发者检查、每个令牌请求中的资源指示器以及结构化的禁止透传规则是核心防御措施。这些不是可以半实现的功能——每一个都是硬性检查,失败时会关闭。
更难的问题在于操作方面。部署 MCP 服务器的团队需要:
- 在
/.well-known/oauth-protected-resource发布有效的 PRM 文档,并在文档中包含一个resource字段,其值与他们服务器的规范 URI 相匹配 - 使用支持资源指示器的 AS — 许多身份提供者仍不支持,或者将
resource参数视为建议而非受众绑定 - 在弃用前,从 DCR 转到 CIMD,或显式预注册凭证
- 将凭证固定到发行者,以防在 AS 拓扑更改时默默跨发行者重用