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:
+28
-10
@@ -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 天)+ 到期重走 OAuth(owner 重輸祕密)」,兼顧安全
|
||||
(週期性重認證)與簡潔(無長效機密落地)。TTL 由 `MCP_TOKEN_TTL` 調。
|
||||
面積都大。**有意取捨**=改採「較長 TTL 的 access_token + 到期重走 OAuth(owner 重輸一次 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 store:authorization code 一次性(consume 後失效,防重放)、access token 存取 + `exp` 過期、key 為 hash。
|
||||
- metadata:origin 推導、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-key(mock 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 deploy,mistakes #23 codeload 陷阱下走的路徑)→ 需手動**:
|
||||
`wrangler kv namespace create OAUTH_MCP` → 把 id 貼進 `mcp/wrangler.toml` 的 `[[kv_namespaces]] OAUTH_KV`。
|
||||
2. **設 owner secret(CF Secrets)**:`wrangler secret put MCP_OWNER_SECRET`(輸入只有你知道的強祕密)。
|
||||
3.(選配)**設本機相容 static token**:`wrangler secret put MCP_STATIC_TOKEN`,並把本機 `.mcp.json` 的
|
||||
`Authorization: Bearer <此值>`(取代舊明碼 namespace)。
|
||||
|
||||
Reference in New Issue
Block a user