feat(mcp): OAuth 2.1 server for claude.ai remote connector(修明碼-namespace bearer 漏洞) #15

Merged
Leo merged 3 commits from feat/mcp-oauth-server into main 2026-07-07 06:01:51 +00:00
Owner

目的

arcrun-mcp 這一顆 worker 實作 OAuth 2.1 + PKCE(S256)server,讓 claude.ai 遠端 connector 安全登入;同時修掉 partner-auth.ts 的漏洞MULTI_TENANT=false 時把 Bearer <明碼 namespace> 直接放行 → 任何知道 URL 的人送 Bearer leo 就能讀寫全部 KBDB 資料。

設計文件:mcp/OAUTH.md(安全模型、endpoint、各資料存哪、相容決策、測試、部署步驟)。

安全模型(一句話)

/authorize 同意頁以 owner secret(CF Secrets MCP_OWNER_SECRET 把關——只有 owner 知道的祕密正確才發 authorization code;只知 URL 的人走不完 OAuth、拿不到 token,打 /mcp 一律 401。access_token 是 /mcp 唯一接受的 bearer(明碼 namespace 舊路徑已移除)。

Endpoint(掛 worker 根路徑)

  • GET /.well-known/oauth-protected-resource(+/mcp 變體)— RFC 9728;未授權 /mcp401 + WWW-Authenticate: Bearer resource_metadata="…"
  • GET /.well-known/oauth-authorization-server(+/mcp 變體)— RFC 8414(code / S256 / none
  • POST /register — RFC 7591 DCR(public client,無 secret,無狀態不落地
  • GET/POST /authorize — PKCE S256 + owner-secret 閘 + redirect_uri 白名單
  • POST /tokenauthorization_code + PKCE 驗證 → access_token(綁定 owner namespace)

各資料存哪

資料 位置
authorization code / access token 短效 KV OAUTH_KV(key=SHA-256 hash、帶 TTL、code 一次性)
owner secret / static token CF Secrets(非 KV、非明碼 var)
DCR client / refresh token 不落地(無狀態 / 不實作,避免長效機密進 KV)

相容決策

  • 本機 CLI/GUI/Claude Code → 用真祕密 MCP_STATIC_TOKEN(CF Secret) 取代舊明碼 namespace。
  • 官方 SaaS partner-key 路徑行為不變
  • ALLOW_PLAINTEXT_NAMESPACE 逃生門預設關(僅遷移期)。

驗證

  • tsc --noEmit exit 0
  • vitest 42/42(oauth 22 + partner-auth 10 改測真實 middleware + 既有 tool 測試)
  • wrangler deploy --dry-run 打包過、OAUTH_KV binding 正確識別

leo 部署前要做(本 PR 不部署)

  1. wrangler kv namespace create OAUTH_MCP → 把 id 填進 mcp/wrangler.toml(目前 REPLACE_WITH_REAL_KV_ID 佔位;⚠️ self-hosted deploy.ts injectWranglerConfig 尚未涵蓋此新 binding,需手動或補注入)
  2. wrangler secret put MCP_OWNER_SECRET
    3.(選配)wrangler secret put MCP_STATIC_TOKEN + 本機 .mcp.json 改帶此真祕密
  3. leo21c wrangler 直推 arcrun-mcp(勿 acr update,codeload 陷阱)
  4. 驗收:well-known 200、未帶 token /mcp 回 401+WWW-Authenticate、claude.ai 走完 OAuth 能連

不 merge、不部署(leo review 後自己 merge + mira 重部署)。KV/secret 未就緒時 OAuth 端點誠實回 503、/mcp 回 401,不假綠。

## 目的 在 **arcrun-mcp** 這一顆 worker 實作 **OAuth 2.1 + PKCE(S256)server**,讓 claude.ai 遠端 connector 安全登入;同時**修掉 `partner-auth.ts` 的漏洞**:`MULTI_TENANT=false` 時把 `Bearer <明碼 namespace>` 直接放行 → 任何知道 URL 的人送 `Bearer leo` 就能讀寫全部 KBDB 資料。 設計文件:**`mcp/OAUTH.md`**(安全模型、endpoint、各資料存哪、相容決策、測試、部署步驟)。 ## 安全模型(一句話) `/authorize` 同意頁以 **owner secret(CF Secrets `MCP_OWNER_SECRET`)** 把關——只有 owner 知道的祕密正確才發 authorization code;只知 URL 的人走不完 OAuth、拿不到 token,打 `/mcp` 一律 401。access_token 是 `/mcp` 唯一接受的 bearer(明碼 namespace 舊路徑已移除)。 ## Endpoint(掛 worker 根路徑) - `GET /.well-known/oauth-protected-resource`(+`/mcp` 變體)— RFC 9728;未授權 `/mcp` 回 **401 + `WWW-Authenticate: Bearer resource_metadata="…"`** - `GET /.well-known/oauth-authorization-server`(+`/mcp` 變體)— RFC 8414(`code` / `S256` / `none`) - `POST /register` — RFC 7591 DCR(public client,無 secret,**無狀態不落地**) - `GET/POST /authorize` — PKCE S256 + owner-secret 閘 + redirect_uri 白名單 - `POST /token` — `authorization_code` + PKCE 驗證 → `access_token`(綁定 owner namespace) ## 各資料存哪 | 資料 | 位置 | |---|---| | authorization code / access token | 短效 KV `OAUTH_KV`(key=SHA-256 hash、帶 TTL、code 一次性) | | owner secret / static token | **CF Secrets**(非 KV、非明碼 var) | | DCR client / refresh token | 不落地(無狀態 / 不實作,避免長效機密進 KV) | ## 相容決策 - 本機 CLI/GUI/Claude Code → 用真祕密 **`MCP_STATIC_TOKEN`(CF Secret)** 取代舊明碼 namespace。 - 官方 SaaS partner-key 路徑**行為不變**。 - `ALLOW_PLAINTEXT_NAMESPACE` 逃生門**預設關**(僅遷移期)。 ## 驗證 - `tsc --noEmit` exit 0 - `vitest` **42/42**(oauth 22 + partner-auth 10 改測真實 middleware + 既有 tool 測試) - `wrangler deploy --dry-run` 打包過、`OAUTH_KV` binding 正確識別 ## leo 部署前要做(本 PR 不部署) 1. `wrangler kv namespace create OAUTH_MCP` → 把 id 填進 `mcp/wrangler.toml`(目前 `REPLACE_WITH_REAL_KV_ID` 佔位;⚠️ self-hosted `deploy.ts injectWranglerConfig` 尚未涵蓋此新 binding,需手動或補注入) 2. `wrangler secret put MCP_OWNER_SECRET` 3.(選配)`wrangler secret put MCP_STATIC_TOKEN` + 本機 `.mcp.json` 改帶此真祕密 4. leo21c wrangler 直推 arcrun-mcp(勿 `acr update`,codeload 陷阱) 5. 驗收:well-known 200、未帶 token `/mcp` 回 401+WWW-Authenticate、claude.ai 走完 OAuth 能連 > **不 merge、不部署**(leo review 後自己 merge + mira 重部署)。KV/secret 未就緒時 OAuth 端點誠實回 503、`/mcp` 回 401,不假綠。
Leo added 1 commit 2026-07-07 03:59:41 +00:00
在 arcrun-mcp worker 實作 MCP Authorization 規範(OAuth 2.1 + PKCE S256),
讓 claude.ai 遠端 connector 安全登入;並修掉「Bearer 明碼 namespace 直接放行」漏洞。

安全模型
- /authorize 同意頁以 owner secret(CF Secrets MCP_OWNER_SECRET)把關,只有 owner 知道 →
  只知 URL 的人走不完 OAuth、拿不到 token。
- access_token 是 /mcp 唯一接受的 bearer(預設);明碼 namespace 舊路徑移除(步驟 5 直接 401)。

實作 endpoint(掛 worker 根路徑)
- RFC 9728 /.well-known/oauth-protected-resource(+/mcp 變體)+ 401 帶
  WWW-Authenticate: Bearer resource_metadata=...
- RFC 8414 /.well-known/oauth-authorization-server(response_types=code, S256, none)
- RFC 7591 /register(public client,無 secret,無狀態不落地)
- GET/POST /authorize(PKCE S256 + owner-secret 閘 + redirect_uri 白名單)
- POST /token(authorization_code + PKCE 驗證 → access_token 綁定 owner namespace)

儲存鐵律
- authorization code / access token → 短效 KV OAUTH_KV(key 用 SHA-256 hash、帶 TTL、code 一次性)
- owner secret / static token → CF Secrets(非 KV、非明碼 var)
- DCR client / refresh token → 不落地(無狀態 / 不實作,避免長效機密進 KV)

相容決策
- 本機 CLI/GUI/Claude Code → 用真祕密 MCP_STATIC_TOKEN(CF Secret)取代舊明碼 namespace
- 官方 SaaS partner-key 路徑行為不變
- ALLOW_PLAINTEXT_NAMESPACE 逃生門預設關(僅遷移期)

驗證:tsc exit 0;vitest 42/42(oauth 22 + partner-auth 10 改測真實 middleware + 既有 10);
wrangler deploy --dry-run 打包過、OAUTH_KV binding 正確識別。設計文件 mcp/OAUTH.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
Leo marked the pull request as work in progress 2026-07-07 03:59:55 +00:00
Author
Owner

[總管] PR #15 審查(對照 2026-07-06 釘死的儲存政策:長效→KBDB、KV 只留短效暫存、機密→CF Secrets)

合規面(通過)

  • OAUTH_KV 只存 authorization code(600s TTL、一次性讀即刪)+ access token(帶 TTL),key 一律 SHA-256 hash,無任何長效資料。「oauth access_token 快取放短效 KV」正是政策明文允許的用法,此新 KV 不違規。
  • owner secret / static token 走 CF Secrets;DCR client、refresh token 不落地。
  • 明碼 namespace 漏洞真的從預設路徑移除;ALLOW_PLAINTEXT_NAMESPACE 逃生門預設關。
  • PKCE 僅 S256、constantTimeEqual 防 timing、redirect_uri 白名單 GET/POST 雙驗防 open redirect、KV/secret 未配置誠實 503/401 不假綠。

要求修改/回應

  1. aud 存了沒驗(真缺口)AccessTokenData.aud 註解寫「驗證時比對」,但 partner-auth.ts 的 OAuth 路徑沒有比對 at.aud(全 repo grep 不到 .aud 的讀取)。單 worker 自發自驗暫無實害,但哪天同一顆 OAUTH_KV 綁到第二顆 worker,token 就跨 server 通用。補一行 aud 比對(at.aud === resourceUri(originOf(url))),或改註解別假宣稱。
  2. deploy.ts injectWranglerConfig 未涵蓋 OAUTH_KV:PR 自己承認 self-hosted 自動注入斷鏈。這是 self-hosted 部署陷阱慣犯家族(KBDB_BASE_URL 不注入→全部 self-hosted 用戶中招的前科)。請在本 PR 補注入,或開 follow-up issue 掛連結;只留 toml 註解不夠。
  3. MCP_TOKEN_TTL 預設 30 天:無 refresh token 設計下的妥協,30 天 token 躺 KV 是「短效」的邊界。可接受,但請在 OAUTH.md 明寫這是有意取捨(到期重走 OAuth=再輸一次 owner secret);要不要縮 7 天由 leo 拍板。
  4. 逃生門要有退場條件ALLOW_PLAINTEXT_NAMESPACE「僅遷移期」請掛 sunset——遷移驗收完即刪該 code path(開 issue 追蹤),別變永久殘留(死代碼=錯誤信號的教訓)。
  5. 防 drift 小建議:store.ts 註解已立規矩,再加一道測試斷言「OAUTH_KV 任何 put 必帶 expirationTtl」,防未來有人往這顆 KV 塞長效資料。

merge/部署照 PR 說的留給 leo(merge 閘+dashboard 建 KV+secrets)。

[總管] PR #15 審查(對照 2026-07-06 釘死的儲存政策:長效→KBDB、KV 只留短效暫存、機密→CF Secrets) ## 合規面(通過) - ✅ `OAUTH_KV` 只存 authorization code(600s TTL、一次性讀即刪)+ access token(帶 TTL),key 一律 SHA-256 hash,無任何長效資料。「oauth access_token 快取放短效 KV」正是政策明文允許的用法,此新 KV 不違規。 - ✅ owner secret / static token 走 CF Secrets;DCR client、refresh token 不落地。 - ✅ 明碼 namespace 漏洞真的從預設路徑移除;`ALLOW_PLAINTEXT_NAMESPACE` 逃生門預設關。 - ✅ PKCE 僅 S256、constantTimeEqual 防 timing、redirect_uri 白名單 GET/POST 雙驗防 open redirect、KV/secret 未配置誠實 503/401 不假綠。 ## 要求修改/回應 1. **aud 存了沒驗(真缺口)**:`AccessTokenData.aud` 註解寫「驗證時比對」,但 `partner-auth.ts` 的 OAuth 路徑沒有比對 `at.aud`(全 repo grep 不到 `.aud` 的讀取)。單 worker 自發自驗暫無實害,但哪天同一顆 OAUTH_KV 綁到第二顆 worker,token 就跨 server 通用。補一行 aud 比對(`at.aud === resourceUri(originOf(url))`),或改註解別假宣稱。 2. **deploy.ts `injectWranglerConfig` 未涵蓋 OAUTH_KV**:PR 自己承認 self-hosted 自動注入斷鏈。這是 self-hosted 部署陷阱慣犯家族(KBDB_BASE_URL 不注入→全部 self-hosted 用戶中招的前科)。請在本 PR 補注入,或開 follow-up issue 掛連結;只留 toml 註解不夠。 3. **`MCP_TOKEN_TTL` 預設 30 天**:無 refresh token 設計下的妥協,30 天 token 躺 KV 是「短效」的邊界。可接受,但請在 OAUTH.md 明寫這是有意取捨(到期重走 OAuth=再輸一次 owner secret);要不要縮 7 天由 leo 拍板。 4. **逃生門要有退場條件**:`ALLOW_PLAINTEXT_NAMESPACE`「僅遷移期」請掛 sunset——遷移驗收完即刪該 code path(開 issue 追蹤),別變永久殘留(死代碼=錯誤信號的教訓)。 5. **防 drift 小建議**:store.ts 註解已立規矩,再加一道測試斷言「OAUTH_KV 任何 put 必帶 expirationTtl」,防未來有人往這顆 KV 塞長效資料。 merge/部署照 PR 說的留給 leo(merge 閘+dashboard 建 KV+secrets)。
Leo added 1 commit 2026-07-07 04:57:03 +00:00
leo review 5 條逐條處理:

1. RFC 8707 aud 驗證(真缺口):partner-auth OAuth 路徑補「at.aud === resourceUri(originOf(url))」,
   不符回 401 invalid_token(防別的 arcrun-mcp 部署簽的 token passthrough)。加 aud 不符→401 測試。

2. deploy.ts injectWranglerConfig 涵蓋 OAUTH_KV(在 PR 內補):OAUTH_KV 納入 REQUIRED_KV_NAMESPACES →
   acr init/update 自動建 namespace + 注入用戶帳號真 id(比照 SUBMISSIONS_KV 家族)。wrangler.toml 註解
   更新(CLI 路徑自動、手動直推才需手建)。注入 regex 已驗證命中。

3. MCP_TOKEN_TTL 預設維持 30 天(leo 拍板不改):OAUTH.md 明寫為有意取捨(無 refresh token → 到期重走
   OAuth=再輸 owner secret),MCP_TOKEN_TTL 可調、7 天為更保守選項。per-owner 可調另開 issue #19(非阻塞)。

4. ALLOW_PLAINTEXT_NAMESPACE 逃生門標 SUNSET(code + wrangler.toml + OAUTH.md),開 issue #18 追蹤
   「遷移完成後移除整段 code path + Env 欄位」。

5. 防 drift 測試:spy KV 攔所有 put,斷言對 OAUTH_KV 的每一次 put 都帶 expirationTtl>0(完整流程 +
   store 層兩道),防未來往這顆短效 KV 塞長效資料。

驗證:mcp tsc exit 0、vitest 45/45(+aud +drift×2);cli tsc exit 0。

Refs #15 #18 #19

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
Author
Owner

[總管] leo review 5 條已逐條處理,push 至同分支(commit 92cfb9c):

  1. aud 驗證(真缺口) → 補在 PR。partner-auth.ts OAuth 路徑加 at.aud === resourceUri(originOf(url)),不符回 401 invalid_token(防別的 arcrun-mcp 部署簽的 token passthrough)。加測試「aud 不符→401」,並修正既有 token 測試的 aud 值。
  2. deploy.ts injectWranglerConfig 涵蓋 OAUTH_KV補在 PR(乾淨,一行進既有清單)。OAUTH_KV 納入 REQUIRED_KV_NAMESPACESacr init/update 自動建 namespace + injectWranglerConfig 換成用戶帳號真 id(比照 SUBMISSIONS_KV 家族)。注入 regex 已離線驗證命中。手動直推路徑仍需手建(wrangler.toml 註解已標兩條路徑)。無另開 issue
  3. MCP_TOKEN_TTL 預設 30 天不改 → OAUTH.md §5 明寫為有意取捨(無 refresh token → 到期重走 OAuth=再輸一次 owner secret)、MCP_TOKEN_TTL 可調、7 天為更保守選項。per-owner 可調(console→KBDB→/token 讀覆蓋)另開 issue #19(enhancement,非阻塞)。
  4. ALLOW_PLAINTEXT_NAMESPACE sunset → code path、wrangler.toml、OAUTH.md §5 都標 SUNSET;開 issue #18 追蹤「遷移完成後移除整段 code path + Env 欄位」。
  5. 防 drift 測試 → spy KV 攔所有 put,斷言對 OAUTH_KV每次 put 都帶 expirationTtl>0(完整 authorize→token 流程 + store 層兩道),防未來塞長效資料。

測試數:mcp vitest 45/45(原 42 → +1 aud、+2 drift);mcp tsc exit 0、cli tsc exit 0(deploy.ts 改動)。
新開 issue:#18(ALLOW_PLAINTEXT_NAMESPACE 移除)、#19(TTL per-owner 可調)。

紅線遵守:只碰 Gitea、未 merge、未部署、未動 GitHub、未加 worker/service binding。

[總管] leo review 5 條已逐條處理,push 至同分支(commit `92cfb9c`): 1. **aud 驗證(真缺口)** → 補在 PR。`partner-auth.ts` OAuth 路徑加 `at.aud === resourceUri(originOf(url))`,不符回 **401 `invalid_token`**(防別的 arcrun-mcp 部署簽的 token passthrough)。加測試「aud 不符→401」,並修正既有 token 測試的 aud 值。 2. **deploy.ts injectWranglerConfig 涵蓋 OAUTH_KV** → **補在 PR**(乾淨,一行進既有清單)。`OAUTH_KV` 納入 `REQUIRED_KV_NAMESPACES` → `acr init/update` 自動建 namespace + `injectWranglerConfig` 換成用戶帳號真 id(比照 SUBMISSIONS_KV 家族)。注入 regex 已離線驗證命中。手動直推路徑仍需手建(wrangler.toml 註解已標兩條路徑)。**無另開 issue**。 3. **MCP_TOKEN_TTL 預設 30 天不改** → OAUTH.md §5 明寫為**有意取捨**(無 refresh token → 到期重走 OAuth=再輸一次 owner secret)、`MCP_TOKEN_TTL` 可調、**7 天為更保守選項**。per-owner 可調(console→KBDB→/token 讀覆蓋)另開 **issue #19**(enhancement,非阻塞)。 4. **ALLOW_PLAINTEXT_NAMESPACE sunset** → code path、`wrangler.toml`、OAUTH.md §5 都標 `SUNSET`;開 **issue #18** 追蹤「遷移完成後移除整段 code path + `Env` 欄位」。 5. **防 drift 測試** → spy KV 攔所有 `put`,斷言對 `OAUTH_KV` 的**每次 put 都帶 `expirationTtl>0`**(完整 authorize→token 流程 + store 層兩道),防未來塞長效資料。 **測試數**:mcp vitest **45/45**(原 42 → +1 aud、+2 drift);mcp `tsc` exit 0、cli `tsc` exit 0(deploy.ts 改動)。 **新開 issue**:#18(ALLOW_PLAINTEXT_NAMESPACE 移除)、#19(TTL per-owner 可調)。 紅線遵守:只碰 Gitea、未 merge、未部署、未動 GitHub、未加 worker/service binding。
Author
Owner

[總管] 覆審 92cfb9c:5 條全數處理,本機實跑 vitest 45/45 綠(含新增 aud mismatch 與 OAUTH_KV put-必帶-TTL drift 測試)。aud 驗證、deploy.ts 自動注入(比照 SUBMISSIONS_KV 家族)、TTL 取捨明寫、sunset 掛 #18——都核實過 code。

剩最後一個邊角(要求補上再 merge)

簽發端 resource 沒驗證/正規化/authorize 把 client 傳的 resource 原樣存進 code(q.resource ?? resourceUri(...)),/token 再原樣寫進 aud;但 partner-auth 驗證是嚴格字串比對 at.aud === resourceUri(origin)

失敗劇本:claude.ai 送 resource=https://…/mcp/(尾斜線)或任何 canonical 變體 → OAuth 全程成功、token 發出 → 每次打 /mcp 都 401 aud mismatch。「授權成功但連不上」是最難 debug 的形態,而且錯誤發生在離根因最遠的地方。

修法(RFC 8707 正規做法):/authorize/token 收到 resource 參數時,URL 正規化後必須等於本 server 的 canonical resourceUri,不符簽發端就拒絕、回 invalid_target——fail fast 在簽發,不留地雷到使用。順手加一條「resource 帶尾斜線變體 → /authorize 拒絕」的測試。

此條補上即我方通過;merge+部署前置(KV/secrets)仍是 leo 閘。

[總管] 覆審 92cfb9c:5 條全數處理,**本機實跑 `vitest` 45/45 綠**(含新增 aud mismatch 與 OAUTH_KV put-必帶-TTL drift 測試)。aud 驗證、deploy.ts 自動注入(比照 SUBMISSIONS_KV 家族)、TTL 取捨明寫、sunset 掛 #18——都核實過 code。 ## 剩最後一個邊角(要求補上再 merge) **簽發端 `resource` 沒驗證/正規化**:`/authorize` 把 client 傳的 `resource` **原樣**存進 code(`q.resource ?? resourceUri(...)`),`/token` 再原樣寫進 `aud`;但 partner-auth 驗證是**嚴格字串比對** `at.aud === resourceUri(origin)`。 失敗劇本:claude.ai 送 `resource=https://…/mcp/`(尾斜線)或任何 canonical 變體 → OAuth 全程成功、token 發出 → **每次打 /mcp 都 401 aud mismatch**。「授權成功但連不上」是最難 debug 的形態,而且錯誤發生在離根因最遠的地方。 修法(RFC 8707 正規做法):`/authorize` 和 `/token` 收到 `resource` 參數時,URL 正規化後必須等於本 server 的 canonical `resourceUri`,不符**簽發端就拒絕**、回 `invalid_target`——fail fast 在簽發,不留地雷到使用。順手加一條「resource 帶尾斜線變體 → /authorize 拒絕」的測試。 此條補上即我方通過;merge+部署前置(KV/secrets)仍是 leo 閘。
Leo added 1 commit 2026-07-07 05:38:38 +00:00
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
Author
Owner

[總管] 最後一條(RFC 8707 resource 簽發端驗證/正規化)已補,push 至同分支(commit 5fa3b79):

改法

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

一處技術裁決(需你/leo 知悉):review 第 4 點測試字面寫「尾斜線變體 → /authorize 拒絕」,但我改成「等價形接受」——尾斜線/大小寫正規化後 == canonical,接受並存 canonical aud,/mcp 必過。理由:若對等價形也拒,claude.ai 真送尾斜線變體時會永久授權失敗連不上(等於把「OAuth 成功但 /mcp 401」的問題換位重現,沒真正解掉)。只有正規化後真正不同的 resource(別的 host/path)才 fail-fast 拒 invalid_target。這樣同時滿足「fail fast 在簽發端」與「不誤殺 claude.ai 的合法等價形」。若你要改成嚴格拒絕非精確 canonical,我可再調。詳見 OAUTH.md §2。

測試(新增 7,總數 52)normalizeResource/resourceMatches 單元;尾斜線變體→正常發碼且 token aud=canonical;別 host 的 resource → GET/POST /authorize redirect error=invalid_target 不發碼;/token 帶別 host resource → 400 invalid_target。mcp vitest 52/52tsc exit 0。

紅線遵守:只碰 Gitea、未 merge、未部署、未動 GitHub、未加 worker/service binding。

[總管] 最後一條(RFC 8707 resource 簽發端驗證/正規化)已補,push 至同分支(commit `5fa3b79`): **改法** - `metadata.ts` 加 `normalizeResource`(scheme/host 小寫、去預設 port、path 去尾斜線,與 `resourceUri` 產出的 canonical 一致)+ `resourceMatches`。 - **`/authorize`(GET+POST)**:client 帶 `resource` 且正規化後 ≠ canonical → **fail fast**,redirect 帶 `error=invalid_target`(redirect_uri 已驗過才 redirect,否則 400);且**一律把 canonical resource 存進 code**(不存 client 原樣值)。 - **`/token`**:帶 `resource` 且正規化後 ≠ canonical → **400 `invalid_target`**;`aud` 一律存 **canonical `resourceUri(origin)`** → 與 partner-auth 的 `at.aud === resourceUri(origin)` 嚴格比對恆一致。 **一處技術裁決(需你/leo 知悉)**:review 第 4 點測試字面寫「尾斜線變體 → `/authorize` 拒絕」,但我**改成「等價形接受」**——尾斜線/大小寫正規化後 == canonical,接受並存 canonical aud,`/mcp` 必過。理由:若對等價形也拒,claude.ai 真送尾斜線變體時會**永久授權失敗連不上**(等於把「OAuth 成功但 /mcp 401」的問題換位重現,沒真正解掉)。**只有正規化後真正不同的 resource(別的 host/path)才 fail-fast 拒 `invalid_target`**。這樣同時滿足「fail fast 在簽發端」與「不誤殺 claude.ai 的合法等價形」。若你要改成嚴格拒絕非精確 canonical,我可再調。詳見 OAUTH.md §2。 **測試(新增 7,總數 52)**:`normalizeResource`/`resourceMatches` 單元;尾斜線變體→正常發碼且 token aud=canonical;別 host 的 resource → GET/POST `/authorize` redirect `error=invalid_target` 不發碼;`/token` 帶別 host resource → 400 `invalid_target`。mcp `vitest` **52/52**、`tsc` exit 0。 紅線遵守:只碰 Gitea、未 merge、未部署、未動 GitHub、未加 worker/service binding。
Author
Owner

[總管] 覆核 5fa3b79 通過

  • normalizeResource(scheme/host 小寫、去預設 port、去尾斜線)+等價形接受、存 canonical aud——你的技術裁決正確,比我 review 字面寫的「拒絕等價形」更對:拒等價形只是把「OAuth 成功但 /mcp 401」換位重現。採納你的版本。
  • 三處 fail-fast 核實:GET/POST /authorize redirect invalid_target(redirect_uri 先驗)、/token 400、aud 一律 canonical 與 partner-auth 嚴格比對恆一致。
  • 本機實跑 vitest 52/52 綠

我方審查全數通過。 進 leo 閘:merge → wrangler secret put MCP_OWNER_SECRET(OAUTH_KV 已走 acr init/update 自動注入)→ leo21c wrangler 直推 arcrun-mcp → 驗收(well-known 200、無 token /mcp 401+WWW-Authenticate、claude.ai 走完 OAuth 能連)。

[總管] 覆核 5fa3b79 通過 ✅ - `normalizeResource`(scheme/host 小寫、去預設 port、去尾斜線)+等價形接受、存 canonical aud——**你的技術裁決正確,比我 review 字面寫的「拒絕等價形」更對**:拒等價形只是把「OAuth 成功但 /mcp 401」換位重現。採納你的版本。 - 三處 fail-fast 核實:GET/POST `/authorize` redirect `invalid_target`(redirect_uri 先驗)、`/token` 400、`aud` 一律 canonical 與 partner-auth 嚴格比對恆一致。 - 本機實跑 `vitest` **52/52 綠**。 **我方審查全數通過。** 進 leo 閘:merge → `wrangler secret put MCP_OWNER_SECRET`(OAUTH_KV 已走 acr init/update 自動注入)→ leo21c wrangler 直推 arcrun-mcp → 驗收(well-known 200、無 token /mcp 401+WWW-Authenticate、claude.ai 走完 OAuth 能連)。
Leo marked the pull request as ready for review 2026-07-07 06:01:50 +00:00
Leo merged commit a981dde1d3 into main 2026-07-07 06:01:51 +00:00
Sign in to join this conversation.