fix(mcp): address PR #15 review — aud validation, OAUTH_KV auto-inject, TTL/​sunset docs, drift test

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
This commit is contained in:
Claude
2026-07-07 04:56:59 +00:00
parent 7d9d478baa
commit 92cfb9c59f
6 changed files with 166 additions and 19 deletions
+28 -10
View File
@@ -75,7 +75,7 @@ metadata 以「當前請求 origin」動態生成 → 同一份碼在 `mcp.arcru
2. **`MCP_STATIC_TOKEN`(真祕密,CF Secret** → 解成 `MCP_OWNER_NAMESPACE`。**本機 CLI / GUI /
本機 Claude Code 的相容路徑**——用「真祕密 token」取代舊「明碼 namespace」,owner 可掌控(CF Secrets)。
3. **官方 SaaS`MULTI_TENANT` 未設/`true`** → KBDB partner-key 驗證,**行為完全不變**。
4. **【預設關】`ALLOW_PLAINTEXT_NAMESPACE="true"`** → 恢復舊明碼路徑。**僅遷移期**,設了等於重開漏洞,正式勿用。
4. **【預設關SUNSET`ALLOW_PLAINTEXT_NAMESPACE="true"`** → 恢復舊明碼路徑。**僅遷移期**,設了等於重開漏洞,正式勿用。
5. 皆不符 → 401 + `WWW-Authenticate`
**明碼 namespace 當 bearer 的舊路徑已從預設移除**(步驟 5 直接 401)。之所以保留步驟 2/4:
@@ -84,13 +84,26 @@ metadata 以「當前請求 origin」動態生成 → 同一份碼在 `mcp.arcru
明碼 namespace 屬 CLI 側後續(本 PR 未動 CLI,於報告標為待辦)。
- `ALLOW_PLAINTEXT_NAMESPACE` 只是遷移期的明確 opt-in 逃生門,預設關 = 預設安全。
**逃生門有退場(SUNSET**`ALLOW_PLAINTEXT_NAMESPACE` 只是遷移期暫時相容,**驗收完即刪整段 code path
`Env` 欄位**。移除追蹤:**Gitea issue #18**https://git.uncle6.me/Leo/Arcrun/issues/18)。code 中該分支已標
`SUNSET` 註記。
### access_token TTL 是有意取捨(無 refresh token
**為何不做 refresh token**refresh token 需長效持久化,依鐵律得進 CF Secrets/KBDB 而非 KV,成本與
面積都大。改採「較長 TTL 的 access_token(預設 30 天)+ 到期重走 OAuthowner 重輸祕密)」,兼顧安全
(週期性重認證)與簡潔(無長效機密落地)。TTL 由 `MCP_TOKEN_TTL` 調。
面積都大。**有意取捨**改採「較長 TTL 的 access_token + 到期重走 OAuthowner 重輸一次 owner secret)」,
兼顧安全(週期性重認證)與簡潔(無長效機密落地)。
- **預設 `MCP_TOKEN_TTL` = 2592000 秒(30 天)**——本 PR 維持此預設(leo 拍板不改)。
- **可調**`[vars]``MCP_TOKEN_TTL` 即可;**7 天(604800)為更保守選項**(縮短 = 更頻繁重認證 = 更安全但 UX 略煩)。
- **到期行為**KV TTL 到 → token 自動失效 → `/mcp` 回 401 + `WWW-Authenticate` → claude.ai 重走 OAuth
(再輸一次 owner secret)。無 refresh token 故無長效機密落地。
- **後續(per-owner 可調,非本 PR)**TTL 風險偏好交用戶決定——console 設定頁 → 存 KBDB → `/token` 發 token
時讀 per-owner 覆蓋、回退 30 天。追蹤:**Gitea issue #19**https://git.uncle6.me/Leo/Arcrun/issues/19)。
## 6. 測試涵蓋
`mcp/tests/unit/oauth.test.ts`22
`mcp/tests/unit/oauth.test.ts`
- 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` 格式。
@@ -99,22 +112,27 @@ metadata 以「當前請求 origin」動態生成 → 同一份碼在 `mcp.arcru
白名單擋非法 host、GET `/authorize` 要 PKCE、`MCP_OWNER_SECRET` 未設→503、**正確祕密+正確 verifier→
access_token**、**錯誤祕密→401 不發 code**、錯誤 verifier→invalid_grant、重用 code→invalid_grant、
`OAUTH_KV` 未設→503。
- **防 drift**spy KV 攔所有 `put`,斷言對 `OAUTH_KV` 的**每一次 put 都帶 `expirationTtl`> 0**——
防未來有人往這顆 KV 塞長效資料(守儲存鐵律)。
`mcp/tests/unit/partner-auth.test.ts`10改測真實 middleware):
`mcp/tests/unit/partner-auth.test.ts`(改測真實 middleware):
- 無/壞 Authorization → 401 + `WWW-Authenticate`RFC 9728)。
- OAuth token → 解出 namespace;未知 token(明碼)在 self-hosted → **401(洞已補)**
- **RFC 8707 aud 驗證**token `aud` 不等於本次請求 origin 算出的 canonical resource URI → **401 `invalid_token`**
- `MCP_STATIC_TOKEN` 相容路徑通過 / 明碼被擋。
- 官方 SaaS partner-keymock KBDB)行為不變。
- `ALLOW_PLAINTEXT_NAMESPACE` 逃生門開/關。
全部 42 tests 綠;`tsc --noEmit` exit 0`wrangler deploy --dry-run` 打包過、`OAUTH_KV` binding 正確識別。
全部測試綠;`tsc --noEmit` exit 0`wrangler deploy --dry-run` 打包過、`OAUTH_KV` binding 正確識別。
## 7. leo 部署前要做什麼(不在本 PR 內,本 PR 不部署)
1. **建 KV namespace 填 id**`wrangler kv namespace create OAUTH_MCP` → 把 id 貼進
`mcp/wrangler.toml` `[[kv_namespaces]] OAUTH_KV`(目前是 `REPLACE_WITH_REAL_KV_ID` 佔位)。
> self-hosted 自動注入(`deploy.ts injectWranglerConfig`**尚未涵蓋此新 binding** → 需手動填,或
> 補 injectWranglerConfig 加一條(跨 CLI 的後續,見報告待辦)
1. **建 KV namespace + 填 id**(依部署路徑):
- **CLI 路徑(`acr init` / `acr update`)→ 已自動化**:本 PR 把 `OAUTH_KV` 納入 `deploy.ts`
`REQUIRED_KV_NAMESPACES` → init/update 自動建 namespace(冪等)+ `injectWranglerConfig`
`REPLACE_WITH_REAL_KV_ID` 換成用戶帳號真 id。零手動
- **手動直推(leo21c wrangler deploymistakes #23 codeload 陷阱下走的路徑)→ 需手動**:
`wrangler kv namespace create OAUTH_MCP` → 把 id 貼進 `mcp/wrangler.toml``[[kv_namespaces]] OAUTH_KV`
2. **設 owner secretCF Secrets**`wrangler secret put MCP_OWNER_SECRET`(輸入只有你知道的強祕密)。
3.(選配)**設本機相容 static token**`wrangler secret put MCP_STATIC_TOKEN`,並把本機 `.mcp.json`
`Authorization: Bearer <此值>`(取代舊明碼 namespace)。