22
33> 基于章节12.5(与OAuth的区别)和章节13(现有代码改造点清单),结合DPoP抽象设计,制定的完整改造计划
44
5+ ** 相关文档** :` docs/authorization-multiprotocol.md ` (多协议设计与用法)、` docs/dpop-nonce-implementation-plan.md ` (DPoP nonce 实现方案)、` mcp/client/auth/multi-protocol-design.md ` (顶层设计)
6+
57## 一、改造目标
68
79### 1.1 核心目标
@@ -277,7 +279,7 @@ DPoP作为独立的通用组件,协议可以选择性使用:
277279** 优先级** : 🟡 中
278280
279281** 改造内容** :
280- 1 . ** 新增统一能力发现端点支持**
282+ 1 . ** 新增统一能力发现端点支持** (发现顺序取舍见 ** 十一、11.4 ** )
281283 ``` python
282284 async def discover_authorization_servers (
283285 resource_url : str ,
@@ -427,7 +429,7 @@ DPoP作为独立的通用组件,协议可以选择性使用:
427429 # 4. 发送请求
428430 response = yield request
429431
430- # 5. 处理401/403响应
432+ # 5. 处理401/403响应(OAuth 分支通过 oauth_401_flow_generator 驱动,取舍见十一、11.1)
431433 if response.status_code == 401 :
432434 await self ._handle_401_response(response, request)
433435 elif response.status_code == 403 :
@@ -437,6 +439,7 @@ DPoP作为独立的通用组件,协议可以选择性使用:
4374392 . ** OAuthClientProvider 保持为 OAuth 逻辑唯一实现(最大程度复用)**
438440 - ** 不** 将 OAuth 逻辑迁出到 OAuth2Protocol;新增 ` run_authentication(http_client, ...) ` 供多协议路径调用
439441 - 保持现有 API 不变(向后兼容);OAuth2Protocol 为薄适配层,内部委托 ` OAuthClientProvider.run_authentication `
442+ - 取舍原因见 ** 十一、设计取舍与方案说明 11.1**
440443
4414443 . ** 协议上下文扩展**
442445 ``` python
@@ -529,6 +532,8 @@ DPoP作为独立的通用组件,协议可以选择性使用:
529532
530533#### 3.2.7 API Key 认证方案约定(方案 A)
531534
535+ ** 取舍** :采用 X-API-Key + 可选 Bearer,不解析非标准 ` ApiKey ` scheme。取舍原因见 ** 十一、11.2** 。
536+
532537** 约定** (与前述 3.2.5 协议特定的实现示例一致):
533538- ** 标准兼容** :不解析非标准 ` Authorization: ApiKey <key> ` (` ApiKey ` 非 IANA 注册 scheme);API Key 使用标准 ` Bearer ` 或专用 header。
534539- ** 服务端** :优先从 ` X-API-Key ` header 读取;可选从 ` Authorization: Bearer <key> ` 读取并在 ` valid_keys ` 中查找;由 ` MultiProtocolAuthBackend ` 的验证器顺序区分(OAuthTokenVerifier 先尝试 Bearer → TokenVerifier,APIKeyVerifier 再尝试 X-API-Key / Bearer-in-valid_keys)。
@@ -1043,6 +1048,8 @@ DPoP作为独立的通用组件,协议可以选择性使用:
10431048 - 依赖:协议抽象接口
10441049 - 注意:这是可选功能,可以跳过
10451050
1051+ ** DPoP Nonce** :阶段4 完成后可按 ` docs/dpop-nonce-implementation-plan.md ` 实现 RS/Client/AS 侧 nonce 支持;与当前 DPoP 基础实现正交。
1052+
10461053### 4.4 依赖关系图
10471054
10481055``` mermaid
@@ -1301,6 +1308,8 @@ graph TD
130113085 . ** 渐进式迁移** :可以逐步启用新功能
130213096 . ** 最小化接口** :基础接口只包含必需方法,可选功能通过扩展接口实现
13031310
1311+ ** 设计取舍** :OAuth 薄适配层、Generator 驱动 401 流程、API Key 方案 A、协议发现顺序、DPoP nonce 风险化解等详见 ** 十一、设计取舍与方案说明** 。
1312+
13041313** 预计时间** :
13051314- 核心功能:7周(阶段1-3)
13061315- 完整功能(含DPoP):9周(阶段1-5,阶段4可选)
@@ -1310,3 +1319,90 @@ graph TD
13101319- 熟悉Python异步编程
13111320- 熟悉HTTP协议和RESTful API设计
13121321- 熟悉测试驱动开发
1322+
1323+ ---
1324+
1325+ ## 十一、设计取舍与方案说明
1326+
1327+ 本节汇总历史讨论中的关键设计决策,说明多方案并存时的取舍原因,便于后续实现与评审时对齐。
1328+
1329+ ### 11.1 OAuth 逻辑复用与 401 流程驱动
1330+
1331+ 多协议下的 OAuth 集成涉及两个相关联的取舍:** 逻辑归属** (薄适配层 vs 逻辑迁移)与 ** 401 流程驱动方式** (Generator vs 新建 HTTP 客户端)。
1332+
1333+ ** 逻辑归属 — 可选方案** :
1334+ - ** 方案 A** :将 OAuth 逻辑迁出到 ` OAuth2Protocol ` ,` OAuthClientProvider ` 仅作为遗留入口
1335+ - ** 方案 B** :` OAuth2Protocol ` 为薄适配层,内部委托 ` OAuthClientProvider.run_authentication ` ,OAuth 逻辑保持在 ` oauth2.py `
1336+
1337+ ** 401 流程驱动 — 可选方案** :
1338+ - ** 方案 A** :在 401 处理分支内新建 ` httpx.AsyncClient ` ,独立发送 OAuth 相关请求
1339+ - ** 方案 B** :使用共享的 ` oauth_401_flow_generator ` ,由 ` MultiProtocolAuthProvider ` 驱动,所有 OAuth 步骤通过 ` yield ` 请求交由同一 ` http_client ` 发送
1340+
1341+ ** 取舍** :二者均采用 ** 方案 B** (薄适配层 + Generator 驱动)。
1342+
1343+ ** 原因** :
1344+ 1 . 最大程度复用现有 OAuth 实现,降低迁移风险与回归面;` OAuthClientProvider ` 仍为 OAuth 逻辑唯一实现,避免双轨维护
1345+ 2 . 薄适配层通过 ` run_authentication(http_client, ...) ` 调用,自然要求由调用方传入 ` http_client ` ;Generator 模式使 ` MultiProtocolAuthProvider ` 作为驱动方,用同一 ` http_client ` 发送所有 OAuth 请求,二者设计上互锁
1346+ 3 . 避免在 httpx 认证流程中创建新客户端导致的锁死锁风险;请求统一由 ` httpx.Client(auth=provider) ` 使用的同一 ` http_client ` 发送,行为可预测
1347+ 4 . OAuth 流程(AS 发现、注册、授权、Token 交换)全部由 generator 产出请求,驱动方负责发送并回传响应;现有 ` OAuthClientProvider ` 用户无需改动
1348+
1349+ ### 11.2 API Key 认证方案:标准 scheme vs 自定义 scheme
1350+
1351+ ** 可选方案** :
1352+ - ** 方案 A** :使用 ` X-API-Key ` + 可选 ` Authorization: Bearer <key> ` ,不解析非标准 ` Authorization: ApiKey <key> `
1353+ - ** 方案 B** :使用自定义 ` Authorization: ApiKey <key> ` scheme
1354+
1355+ ** 取舍** :采用 ** 方案 A** 。
1356+
1357+ ** 原因** :
1358+ 1 . ` ApiKey ` 非 IANA 注册的 HTTP Authentication scheme,方案 B 不符合 HTTP 规范
1359+ 2 . RFC 6750 规定 Bearer token 为 opaque string,使用 ` Bearer ` 承载 API Key 语义合理
1360+ 3 . 不在 token 内加前缀(如 ` apikey:xxx ` );区分由验证器顺序与 ` valid_keys ` 完成,符合 Bearer 不解析 token 内容的约定
1361+
1362+ ### 11.3 Mutual TLS 与 IANA "Mutual" scheme
1363+
1364+ ** 说明** :IANA 注册的 "Mutual" scheme(RFC 8120)表示基于密码的双向认证,与基于客户端证书的 Mutual TLS(mTLS)不同。
1365+
1366+ ** 取舍** :mTLS 在 TLS 握手层处理,不解析 HTTP ` Authorization ` 头;` Mutual TLS ` 验证器从 TLS 连接/握手上下文读取客户端证书并校验。
1367+
1368+ ### 11.4 协议发现顺序:统一端点 vs PRM 优先
1369+
1370+ ** 取舍** :客户端优先请求 ` /.well-known/authorization_servers ` (统一发现);若 404 或空,回退到 PRM 的 ` mcp_auth_protocols ` 。
1371+
1372+ ** 原因** :统一端点便于多协议声明与扩展;PRM 回退保证仅支持 RFC 9728 的 RS 仍可被多协议客户端发现。
1373+
1374+ ### 11.5 授权端点归属:AS 与 RS 的 URL 树
1375+
1376+ | 端点 | 归属 | 用途 |
1377+ | ------| ------| ------|
1378+ | ` /.well-known/oauth-authorization-server ` | AS | OAuth 元数据(RFC 8414) |
1379+ | ` /authorize ` , ` /token ` , ` /register ` , ` /introspect ` | AS | OAuth 流程 |
1380+ | ` /.well-known/oauth-protected-resource{path} ` | RS | PRM(RFC 9728) |
1381+ | ` /.well-known/authorization_servers ` | RS | 统一协议发现(MCP 扩展) |
1382+
1383+ ** 说明** :AS 与 RS 可能部署在不同主机(如 AS 9000、RS 8002);客户端先向 RS 获取 PRM/协议列表,再根据 ` metadata_url ` 向 AS 获取 OAuth 元数据。
1384+
1385+ ### 11.6 TokenStorage 双契约:OAuthToken vs AuthCredentials
1386+
1387+ ** 取舍** :` TokenStorage ` 支持 ` get_tokens() → AuthCredentials | OAuthToken | None ` 与 ` set_tokens(AuthCredentials | OAuthToken) ` ;` MultiProtocolAuthProvider ` 内部负责 OAuthToken 与 OAuthCredentials 的转换。
1388+
1389+ ** 原因** :现有 OAuth 存储只处理 ` OAuthToken ` ;多协议存储需处理 ` APIKeyCredentials ` 等。双契约 + 内部转换使 OAuth 存储无需改造即可工作。
1390+
1391+ ### 11.7 DPoP Nonce 实现:风险与方案
1392+
1393+ DPoP nonce 详细方案见 ` docs/dpop-nonce-implementation-plan.md ` 。关键取舍如下:
1394+
1395+ | 风险 | 解决方案 |
1396+ | ------| ----------|
1397+ | ** Token 请求 DPoP 缺失** | 单独 TODO 6a 实现 Token 请求 DPoP 与 400 ` use_dpop_nonce ` 重试,作为 AS nonce 前置依赖 |
1398+ | ** AS 改造范围过大** | 拆分为 TODO 6b(SDK TokenHandler DPoP+nonce)与 TODO 6c(simple-auth 示例 DPoP-bound token),各 ≤300 行 |
1399+
1400+ ** 分阶段** :先 RS + Client nonce(TODO 1–5),后 AS nonce(TODO 6a–6c),降低单次改动量。
1401+
1402+ ### 11.8 测试 skipped 说明
1403+
1404+ 全量回归中约有 95 个 skipped:
1405+ - ** 约 90+** 来自 ` tests/experimental/tasks/test_spec_compliance.py ` :占位测试,内部 ` pytest.skip("TODO") ` ,与多协议改造无关
1406+ - ** 其余** :平台条件(如 Windows 专用、无 ` tee ` 命令)、显式跳过(如 SSE timeout 相关 bug 测试)
1407+
1408+ 改造过程中不修改上述 skip 逻辑。
0 commit comments