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
+27 -1
View File
@@ -70,10 +70,17 @@ describe("partner-auth: 無 / 壞 Authorization → 401 + WWW-AuthenticateRFC
describe("partner-auth: OAuth access token 路徑(遠端 claude.ai", () => {
it("有效 access_token → 解出綁定 namespace", async () => {
const kv = makeKV();
// aud 必須 == 本次請求 origin 的 canonical resource URIapp.request 打 https://mcp.arcrun.dev/mcp
await putAccessToken(
kv,
"good-token",
{ namespace: "leo", client_id: "c1", scope: "mcp", aud: "x", exp: Math.floor(Date.now() / 1000) + 100 },
{
namespace: "leo",
client_id: "c1",
scope: "mcp",
aud: "https://mcp.arcrun.dev/mcp",
exp: Math.floor(Date.now() / 1000) + 100,
},
100,
);
const r = await buildApp(baseEnv({ MULTI_TENANT: "false", OAUTH_KV: kv }))("Bearer good-token");
@@ -81,6 +88,25 @@ describe("partner-auth: OAuth access token 路徑(遠端 claude.ai", () =>
expect((await r.json()).org_namespace).toBe("leo");
});
it("RFC 8707 aud 不符 → 401 invalid_token(防 token passthrough", async () => {
const kv = makeKV();
await putAccessToken(
kv,
"wrong-aud-token",
{
namespace: "leo",
client_id: "c1",
scope: "mcp",
aud: "https://other-mcp.example.com/mcp", // 別的部署簽的 token
exp: Math.floor(Date.now() / 1000) + 100,
},
100,
);
const r = await buildApp(baseEnv({ MULTI_TENANT: "false", OAUTH_KV: kv }))("Bearer wrong-aud-token");
expect(r.status).toBe(401);
expect(r.headers.get("WWW-Authenticate")).toContain("resource_metadata=");
});
it("未知 token(非 OAuth、非 static)在 self-hosted → 401(明碼 namespace 不再放行)", async () => {
const kv = makeKV();
const r = await buildApp(baseEnv({ MULTI_TENANT: "false", OAUTH_KV: kv }))("Bearer leo");