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:
@@ -408,3 +408,89 @@ describe("oauth flow (整合)", () => {
|
||||
expect(r.status).toBe(503);
|
||||
});
|
||||
});
|
||||
|
||||
// ── 防 drift:對 OAUTH_KV 的每一次 put 都必須帶 expirationTtl(守儲存鐵律)──────────
|
||||
// spy KV 記錄所有 put(key,value,opts);跑完整流程後斷言沒有任何一次「無 TTL」的 put,
|
||||
// 防未來有人往這顆短效 KV 塞長效資料(access_token / code 以外的東西)。
|
||||
describe("oauth store drift guard:OAUTH_KV 的 put 一律帶 TTL", () => {
|
||||
function spyKV(): { kv: KVNamespace; puts: Array<{ key: string; opts?: { expirationTtl?: number } }> } {
|
||||
const map = new Map<string, string>();
|
||||
const puts: Array<{ key: string; opts?: { expirationTtl?: number } }> = [];
|
||||
const kv = {
|
||||
async put(key: string, value: string, opts?: { expirationTtl?: number }) {
|
||||
puts.push({ key, opts });
|
||||
map.set(key, value);
|
||||
},
|
||||
async get(key: string) {
|
||||
return map.get(key) ?? null;
|
||||
},
|
||||
async delete(key: string) {
|
||||
map.delete(key);
|
||||
},
|
||||
} as unknown as KVNamespace;
|
||||
return { kv, puts };
|
||||
}
|
||||
|
||||
it("完整 authorize→token 流程中,OAUTH_KV 的每次 put 都有 expirationTtl>0", async () => {
|
||||
const { kv, puts } = spyKV();
|
||||
const env = baseEnv({ OAUTH_KV: kv });
|
||||
const app = buildApp(env);
|
||||
const { verifier, challenge } = await pkcePair();
|
||||
const redirect = "https://claude.ai/cb";
|
||||
|
||||
const authRes = await app.req("/authorize", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: "c1",
|
||||
redirect_uri: redirect,
|
||||
code_challenge: challenge,
|
||||
code_challenge_method: "S256",
|
||||
owner_secret: "s3cr3t-owner",
|
||||
}).toString(),
|
||||
redirect: "manual",
|
||||
});
|
||||
const code = new URL(authRes.headers.get("location")!).searchParams.get("code")!;
|
||||
await app.req("/token", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
code_verifier: verifier,
|
||||
redirect_uri: redirect,
|
||||
client_id: "c1",
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
// 至少發生了 put(code + token 各一),且每一次都帶 TTL。
|
||||
expect(puts.length).toBeGreaterThanOrEqual(2);
|
||||
for (const p of puts) {
|
||||
expect(p.opts?.expirationTtl, `put ${p.key} 缺 expirationTtl`).toBeTypeOf("number");
|
||||
expect(p.opts!.expirationTtl!).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("直接呼叫 store 層 putAuthCode / putAccessToken 也一律帶 TTL", async () => {
|
||||
const { kv, puts } = spyKV();
|
||||
await putAuthCode(kv, "c", {
|
||||
client_id: "c1",
|
||||
redirect_uri: "https://claude.ai/cb",
|
||||
code_challenge: "cc",
|
||||
code_challenge_method: "S256",
|
||||
scope: "mcp",
|
||||
resource: "https://mcp/mcp",
|
||||
namespace: "leo",
|
||||
});
|
||||
await putAccessToken(
|
||||
kv,
|
||||
"t",
|
||||
{ namespace: "leo", client_id: "c1", scope: "mcp", aud: "https://mcp/mcp", exp: 1 },
|
||||
100,
|
||||
);
|
||||
expect(puts).toHaveLength(2);
|
||||
for (const p of puts) {
|
||||
expect(p.opts?.expirationTtl).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user