一个具体的接入场景
假设你的团队要把一个远程 MCP Server 接入现有办公套件。这个 MCP Server 由第三方厂商托管,地址是 https://mcp.vendor.example/mcp。它不自己签发令牌,而是依赖厂商的 OAuth 授权服务器。客户端拿到 401 后,需要先找到这个授权服务器,再发起授权码流程。
直觉做法是在客户端配置里写死授权服务器地址,或者直接复用办公套件已有的 OAuth 配置。问题往往出在这里:办公套件的授权服务器和这个 MCP Server 背后的授权服务器并不是同一个。客户端拿着用户已有的令牌去访问 MCP Server,服务端返回 401;客户端如果没按规范重新发现,而是继续用旧 issuer 刷新令牌,就会反复失败,甚至把令牌发给错误的授权服务器。
RFC 9728 和 RFC 8414 要解决的正是这个发现过程。前者让受保护资源声明“我是谁、我信任哪些授权服务器”,后者让客户端拿到授权服务器的端点与能力。MCP 规范要求 MCP Server 必须实现 RFC 9728,MCP 客户端必须用它来发现授权服务器。
401 响应如何暴露资源标识
当客户端未携带有效令牌访问 MCP Server 时,服务端返回 401 Unauthorized。RFC 9728 第 5.1 节规定,受保护资源用 WWW-Authenticate 响应头告诉客户端元数据文档的位置。这个头通常带一个 resource_metadata 参数,值是受保护资源元数据的 URL。
客户端拿到这个 URL 后,向它发起 GET 请求,得到一份 JSON 文档。文档里最关键的是 resource 字段,它用绝对 URI 标识这个受保护资源。RFC 9728 要求元数据从受保护资源 URL 确定性推导出的 well-known 位置获取,这样攻击者无法伪造一份声称描述该资源、但实际并不权威的元数据。
文档里还有 authorization_servers 字段,列出该资源信任的授权服务器 issuer。RFC 9728 允许列出多个授权服务器,具体选哪个由客户端决定。MCP 规范要求这个字段至少包含一个授权服务器。
这里有一个容易被忽略的点:resource 字段的值必须是受保护资源的规范标识,而不是客户端请求时用的那个 URL 的简单副本。如果客户端请求的是 https://mcp.vendor.example/mcp,但元数据里 resource 是 https://mcp.vendor.example/,两者不一致时,客户端应当以元数据里的 resource 为准来构造后续请求。
从资源元数据到授权服务器元数据
拿到 authorization_servers 列表后,客户端要选一个 issuer,然后按 RFC 8414 获取该授权服务器的元数据。RFC 8414 定义了一个 well-known 位置,客户端从 issuer 标识推导出元数据 URL,请求后得到包含 authorization_endpoint、token_endpoint、jwks_uri 等字段的 JSON 文档。
RFC 8414 的 issuer 字段是授权服务器的唯一标识。客户端必须校验返回的元数据里的 issuer 与它请求时使用的 issuer 一致。如果不一致,说明这个元数据不是该 issuer 的,客户端不能使用。这一步是防止元数据被替换或缓存串味的关键。
MCP 规范要求 MCP 客户端必须实现 RFC 8707 的资源指示器(Resource Indicators),在授权请求里用 resource 参数显式指定目标资源。这个参数的作用是让授权服务器知道令牌是给哪个资源用的,从而在令牌里绑定受众。没有它,授权服务器可能签发一个受众宽泛的令牌,资源服务器无法判断令牌是否真的给自己用。
下面这张图展示了一个正常流程里,客户端、MCP Server 和授权服务器之间的交互顺序。
flowchart TD
A[客户端请求 MCP Server] --> B{是否携带有效令牌}
B -- 否 --> C[MCP Server 返回 401]
C --> D[读取 WWW-Authenticate 中的 resource_metadata]
D --> E[获取受保护资源元数据]
E --> F[读取 resource 与 authorization_servers]
F --> G[选择 issuer 并获取授权服务器元数据]
G --> H[校验 issuer 一致性]
H --> I[带 resource 参数发起授权请求]
I --> J[授权服务器签发受众绑定的令牌]
J --> K[客户端携带令牌访问 MCP Server]
B -- 是 --> K
图中的关键转折点在 F 和 H。F 决定客户端用哪个资源标识去绑定令牌,H 决定客户端是否信任拿到的授权服务器元数据。这两步出错,后面所有请求都会指向错误的授权服务器。
客户端为何会向错误的授权服务器发起授权
错误授权通常来自三类机制,它们可以单独出现,也可以叠加。
(1)未按 resource 参数绑定授权服务器
客户端可能跳过资源元数据发现,直接使用配置里已有的授权服务器。比如客户端之前接入过同一个厂商的另一个服务,缓存了那个服务的 issuer,于是直接复用。这个 issuer 可能和当前 MCP Server 信任的授权服务器不同。客户端发起授权请求时,如果没有带 resource 参数,授权服务器签发的令牌受众就是宽泛的,或者指向另一个资源。MCP Server 收到令牌后校验受众失败,返回 401。客户端看到 401 又去刷新令牌,循环往复。
RFC 9728 第 7.4 节讨论了受众限制访问令牌。资源服务器应当校验令牌的受众是否包含自己。如果客户端没有用 resource 参数绑定,令牌的受众就可能不匹配。
(2)元数据缓存过期或串味
RFC 9728 第 7.10 节专门讨论元数据缓存。受保护资源元数据可能变化,比如厂商更换了授权服务器。如果客户端缓存了旧的 authorization_servers,并且没有按 HTTP 缓存语义重新验证,就会继续向已经不用的授权服务器发起授权。
缓存串味是另一种情况:客户端用 URL 作为缓存键,但不同资源的元数据 URL 在路径拼接后可能碰撞。比如 https://mcp.vendor.example/.well-known/oauth-protected-resource/mcp 和 https://mcp.vendor.example/.well-known/oauth-protected-resource/mcp/ 被当成同一个键,实际返回的 resource 可能不同。
(3)路径拼接错误
RFC 9728 要求元数据从受保护资源 URL 确定性推导。推导规则涉及把 well-known 路径插入到资源 URL 的路径部分。如果客户端拼接时把查询参数或片段丢掉,或者把路径末尾的斜杠处理错,得到的元数据 URL 可能指向另一个资源。RFC 9728 第 6 节专门讲字符串操作,说明这类拼接必须严格按规则做。
一个常见错误是客户端把 https://mcp.vendor.example/mcp?tenant=acme 的查询参数带进了 well-known 路径,得到 https://mcp.vendor.example/.well-known/oauth-protected-resource/mcp?tenant=acme。服务端可能忽略查询参数返回默认租户的元数据,客户端就拿到了错误的 authorization_servers。
与硬编码端点方案的对比
硬编码授权服务器地址在早期集成里很常见,它的优点是实现简单、启动快、不依赖额外网络请求。缺点是资源与授权服务器的关系被写死在客户端里,厂商更换授权服务器或资源迁移时,客户端必须发版更新。
| 维度 | 硬编码端点 | RFC 9728 + RFC 8414 发现 |
|---|---|---|
| 实现复杂度 | 低,配置即可 | 中,需要处理 401、解析头、获取两级元数据 |
| 资源迁移适应性 | 差,需客户端更新 | 好,资源元数据变化即可反映 |
| 多授权服务器支持 | 需手动配置 | 由 authorization_servers 列表支持 |
| 令牌受众绑定 | 依赖客户端自觉 | 由 resource 参数和资源元数据共同约束 |
| 缓存失效风险 | 低,因为没有缓存 | 中,需要正确处理元数据缓存 |
| 错误排查难度 | 低,配置明确 | 中,涉及多级发现和 issuer 校验 |
| 安全边界 | 依赖配置正确 | 依赖 issuer 校验和资源标识一致性 |
从工程决策看,如果 MCP Server 和授权服务器长期固定、且客户端可控,硬编码可以作为过渡方案。但只要涉及第三方厂商、多租户或资源可能迁移,发现机制就更合适。代价是客户端要正确处理 401、WWW-Authenticate、两级元数据和缓存。
校验点与可观测指标
要让发现流程可靠,客户端需要在几个位置做校验。
(1)解析 WWW-Authenticate 时校验 resource_metadata 的 URL
这个 URL 必须是 HTTPS,且应当与受保护资源同源或由受保护资源明确授权。如果它指向一个完全无关的域,客户端不应盲目请求。RFC 9728 第 7.1 节要求 TLS,第 7.8 节讨论了钓鱼风险。
(2)校验资源元数据里的 resource
客户端应当确认 resource 与它正在访问的资源标识一致。如果不一致,说明元数据可能来自错误的 well-known 位置,或者路径拼接有误。
(3)校验授权服务器元数据里的 issuer
RFC 8414 要求客户端校验返回的 issuer 与请求时使用的 issuer 一致。这一步能发现缓存串味和元数据替换。
(4)授权请求必须带 resource 参数
MCP 规范要求客户端实现 RFC 8707。resource 参数的值应当来自资源元数据里的 resource 字段,而不是客户端自己拼的 URL。
可观测指标方面,客户端和服务端都应记录以下信号:
- 401 响应次数与
WWW-Authenticate头是否包含resource_metadata。 - 资源元数据请求的 URL、返回的
resource和authorization_servers。 - 授权服务器元数据请求的 issuer 与返回的
issuer是否一致。 - 授权请求里
resource参数的值与资源元数据里的resource是否一致。 - 令牌被资源服务器拒绝的次数,以及拒绝原因是否为受众不匹配。
- 元数据缓存的命中率、年龄和重新验证次数。
这些指标能帮助区分三类失败:发现失败、绑定失败和缓存失败。发现失败表现为拿不到元数据或 issuer 不一致;绑定失败表现为令牌受众不匹配;缓存失败表现为元数据年龄过长或重新验证后 issuer 变化。
失败模式与适用边界
发现机制在以下情况会退化或失败。
如果 MCP Server 没有实现 RFC 9728,客户端拿不到 resource_metadata,只能回退到配置。MCP 规范要求 MCP Server 必须实现 RFC 9728,但实际部署中可能有旧版本或非合规实现。
如果授权服务器不支持 RFC 8414,客户端无法自动获取端点,需要手动配置。MCP 规范要求授权服务器必须提供 RFC 8414 元数据,但第三方授权服务器不一定遵守。
如果客户端没有实现 RFC 8707,授权请求不带 resource 参数,令牌受众绑定就依赖授权服务器的默认行为。有些授权服务器会默认绑定到请求里的某个资源,有些不绑定,行为不一致。
如果元数据缓存没有正确处理,资源迁移后客户端会继续用旧授权服务器。RFC 9728 第 7.10 节建议客户端遵循 HTTP 缓存语义,并在必要时重新验证。
如果路径拼接实现不严格,查询参数、片段或末尾斜杠处理错误,客户端会拿到另一个资源的元数据。RFC 9728 第 6 节给出了字符串操作的规则,实现时应当逐条对照。
尚未解决的问题包括:多个授权服务器时客户端如何选择,RFC 9728 把选择责任留给客户端,但没有给出统一策略;元数据签名在客户端侧的验证支持还不普遍;以及跨厂商的资源标识规范化,不同厂商对 resource 的写法可能不同,客户端需要做一致性处理。