fix(mcp): validate/normalize RFC 8707 resource at issuance; store canonical aud

leo review 最後一條:簽發端沒驗證/正規化 resource → 失敗劇本「OAuth 全程成功但每次打 /mcp 401 aud
mismatch」(尾斜線/canonical 變體,錯誤離根因最遠最難 debug)。改在簽發端 fail fast:

- metadata.ts 加 normalizeResource(scheme/host 小寫、去預設 port、path 去尾斜線,與 resourceUri
  canonical 一致)+ resourceMatches。
- /authorize(GET+POST):帶 resource 且正規化後 != canonical → redirect 帶 error=invalid_target
  (redirect_uri 已驗過才 redirect);一律把 canonical resource 存進 code(不存 client 原樣值)。
- /token:帶 resource 且正規化後 != canonical → 400 invalid_target;aud 一律存 canonical
  resourceUri(origin) → 與 partner-auth 嚴格比對 at.aud===resourceUri(origin) 恆一致。

裁決:尾斜線/大小寫等「正規化後等價」的 resource → 接受(存 canonical aud,/mcp 必過),非拒絕——
否則 claude.ai 真送變體會永久授權失敗連不上(把 401 問題換位重現)。只有正規化後真正不同的
resource(別 host/path)才 fail-fast 拒。詳見 OAUTH.md §2。

測試:normalizeResource/resourceMatches 單元 + 尾斜線變體→正常發碼且 aud canonical、別 host→
/authorize redirect invalid_target 不發碼、/token 別 host→400 invalid_target。
mcp vitest 52/52、tsc exit 0。

Refs #15

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
This commit is contained in:
Claude
2026-07-07 05:38:35 +00:00
parent 92cfb9c59f
commit 5fa3b79a4c
4 changed files with 205 additions and 4 deletions
+14 -1
View File
@@ -32,7 +32,16 @@ PKCE,但**走到發碼那步需要 owner secret**,而 secret 只在 CF Secre
- **redirect_uri 白名單**DCR 無狀態,見 §5):預設只允許 `claude.ai``claude.com``anthropic.com`
(含子網域)+ `localhost``http` 僅限本機。擋 open-redirect/釣魚把 code 送去攻擊者。可用
`MCP_ALLOWED_REDIRECT_HOSTS` 調整。
- **token audience 綁定(RFC 8707**codetoken 記 `resource``aud` = 本 MCP server 的 canonical URI。
- **token audience 綁定 + 簽發端驗證RFC 8707**
- `/authorize``/token` 收到 client 的 `resource` 參數時,**正規化後**(scheme/host 小寫、去預設 port、
path 去尾斜線;與 `metadata.ts resourceUri` 產出的 canonical 一致)必須 == 本 server canonical
`resourceUri(origin)`。**不符則簽發端 fail fast**`/authorize` redirect 帶 `error=invalid_target`
redirect_uri 已驗過才 redirect,否則 400)、`/token` 回 400 `invalid_target`
- **存進 `aud` 的一律是 canonical `resourceUri(origin)`**(不存 client 原樣值)→ 與 partner-auth 的嚴格比對
`at.aud === resourceUri(origin)` 恆一致。
- **為何要正規化而非純字串拒**:claude.ai 送 `…/mcp/`(尾斜線)或大小寫變體是等價 canonical → 正規化後接受、
存 canonical aud → `/mcp` 必過。若對等價形也拒,claude.ai 真送變體會**永久授權失敗連不上**(把「OAuth 成功
但 /mcp 401」的問題換位重現)。只有正規化後**真正不同**的 resource(別的 host/path)才 fail-fast 拒。
- **KV key 用 SHA-256 hash**codetoken 不以明碼當 key,KV list 也拿不到可用憑證(比照
`wasi-shim.ts` 只寫短效 oauth2 cache 的精神)。
- **owner secretstatic token 用常數時間比對**:防 timing attack。
@@ -107,6 +116,10 @@ metadata 以「當前請求 origin」動態生成 → 同一份碼在 `mcp.arcru
- PKCE S256 驗證(正確 / 拒 plain / 拒缺省 method / verifier 長度邊界 / 竄改);已知 SHA-256 向量、RFC 7636 附錄範例。
- 短效 KV storeauthorization code 一次性(consume 後失效,防重放)、access token 存取 + `exp` 過期、key 為 hash。
- metadataorigin 推導、Protected Resource / AS Metadata 必要欄位、`WWW-Authenticate` 格式。
- **RFC 8707 resource 正規化/簽發端驗證**`normalizeResource`/`resourceMatches`(尾斜線/大小寫/預設 port
等價、別 host/path 不等價);`/authorize` 尾斜線變體→**正常發碼**且 token aud 為 canonical、
別 host 的 resource → GET/POST `/authorize` redirect `error=invalid_target` 不發碼、`/token` 帶別 host
resource → 400 `invalid_target`
- consent XSS escape(惡意 `state` 不注入)。
- 完整流程整合(Hono `app.request`):well-known 動態 origin、DCR 回 client_id 無 secret、redirect_uri
白名單擋非法 host、GET `/authorize` 要 PKCE、`MCP_OWNER_SECRET` 未設→503、**正確祕密+正確 verifier→