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:
+14
-1
@@ -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)**:code/token 記 `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**:code/token 不以明碼當 key,KV list 也拿不到可用憑證(比照
|
||||
`wasi-shim.ts` 只寫短效 oauth2 cache 的精神)。
|
||||
- **owner secret/static token 用常數時間比對**:防 timing attack。
|
||||
@@ -107,6 +116,10 @@ metadata 以「當前請求 origin」動態生成 → 同一份碼在 `mcp.arcru
|
||||
- PKCE S256 驗證(正確 / 拒 plain / 拒缺省 method / verifier 長度邊界 / 竄改);已知 SHA-256 向量、RFC 7636 附錄範例。
|
||||
- 短效 KV store:authorization code 一次性(consume 後失效,防重放)、access token 存取 + `exp` 過期、key 為 hash。
|
||||
- metadata:origin 推導、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→
|
||||
|
||||
Reference in New Issue
Block a user