feat(mcp): OAuth 2.1 server for claude.ai remote connector(修明碼-namespace bearer 漏洞) #15
Reference in New Issue
Block a user
Delete Branch "feat/mcp-oauth-server"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
目的
在 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 SecretsMCP_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)各資料存哪
OAUTH_KV(key=SHA-256 hash、帶 TTL、code 一次性)相容決策
MCP_STATIC_TOKEN(CF Secret) 取代舊明碼 namespace。ALLOW_PLAINTEXT_NAMESPACE逃生門預設關(僅遷移期)。驗證
tsc --noEmitexit 0vitest42/42(oauth 22 + partner-auth 10 改測真實 middleware + 既有 tool 測試)wrangler deploy --dry-run打包過、OAUTH_KVbinding 正確識別leo 部署前要做(本 PR 不部署)
wrangler kv namespace create OAUTH_MCP→ 把 id 填進mcp/wrangler.toml(目前REPLACE_WITH_REAL_KV_ID佔位;⚠️ self-hosteddeploy.ts injectWranglerConfig尚未涵蓋此新 binding,需手動或補注入)wrangler secret put MCP_OWNER_SECRET3.(選配)
wrangler secret put MCP_STATIC_TOKEN+ 本機.mcp.json改帶此真祕密acr update,codeload 陷阱)/mcp回 401+WWW-Authenticate、claude.ai 走完 OAuth 能連[總管] 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 不違規。ALLOW_PLAINTEXT_NAMESPACE逃生門預設關。要求修改/回應
AccessTokenData.aud註解寫「驗證時比對」,但partner-auth.ts的 OAuth 路徑沒有比對at.aud(全 repo grep 不到.aud的讀取)。單 worker 自發自驗暫無實害,但哪天同一顆 OAUTH_KV 綁到第二顆 worker,token 就跨 server 通用。補一行 aud 比對(at.aud === resourceUri(originOf(url))),或改註解別假宣稱。injectWranglerConfig未涵蓋 OAUTH_KV:PR 自己承認 self-hosted 自動注入斷鏈。這是 self-hosted 部署陷阱慣犯家族(KBDB_BASE_URL 不注入→全部 self-hosted 用戶中招的前科)。請在本 PR 補注入,或開 follow-up issue 掛連結;只留 toml 註解不夠。MCP_TOKEN_TTL預設 30 天:無 refresh token 設計下的妥協,30 天 token 躺 KV 是「短效」的邊界。可接受,但請在 OAUTH.md 明寫這是有意取捨(到期重走 OAuth=再輸一次 owner secret);要不要縮 7 天由 leo 拍板。ALLOW_PLAINTEXT_NAMESPACE「僅遷移期」請掛 sunset——遷移驗收完即刪該 code path(開 issue 追蹤),別變永久殘留(死代碼=錯誤信號的教訓)。merge/部署照 PR 說的留給 leo(merge 閘+dashboard 建 KV+secrets)。
[總管] leo review 5 條已逐條處理,push 至同分支(commit
92cfb9c):partner-auth.tsOAuth 路徑加at.aud === resourceUri(originOf(url)),不符回 401invalid_token(防別的 arcrun-mcp 部署簽的 token passthrough)。加測試「aud 不符→401」,並修正既有 token 測試的 aud 值。OAUTH_KV納入REQUIRED_KV_NAMESPACES→acr init/update自動建 namespace +injectWranglerConfig換成用戶帳號真 id(比照 SUBMISSIONS_KV 家族)。注入 regex 已離線驗證命中。手動直推路徑仍需手建(wrangler.toml 註解已標兩條路徑)。無另開 issue。MCP_TOKEN_TTL可調、7 天為更保守選項。per-owner 可調(console→KBDB→/token 讀覆蓋)另開 issue #19(enhancement,非阻塞)。wrangler.toml、OAUTH.md §5 都標SUNSET;開 issue #18 追蹤「遷移完成後移除整段 code path +Env欄位」。put,斷言對OAUTH_KV的每次 put 都帶expirationTtl>0(完整 authorize→token 流程 + store 層兩道),防未來塞長效資料。測試數:mcp vitest 45/45(原 42 → +1 aud、+2 drift);mcp
tscexit 0、clitscexit 0(deploy.ts 改動)。新開 issue:#18(ALLOW_PLAINTEXT_NAMESPACE 移除)、#19(TTL per-owner 可調)。
紅線遵守:只碰 Gitea、未 merge、未部署、未動 GitHub、未加 worker/service binding。
[總管] 覆審 92cfb9c:5 條全數處理,本機實跑
vitest45/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 的 canonicalresourceUri,不符簽發端就拒絕、回invalid_target——fail fast 在簽發,不留地雷到使用。順手加一條「resource 帶尾斜線變體 → /authorize 拒絕」的測試。此條補上即我方通過;merge+部署前置(KV/secrets)仍是 leo 閘。
[總管] 最後一條(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 → 400invalid_target;aud一律存 canonicalresourceUri(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/authorizeredirecterror=invalid_target不發碼;/token帶別 host resource → 400invalid_target。mcpvitest52/52、tscexit 0。紅線遵守:只碰 Gitea、未 merge、未部署、未動 GitHub、未加 worker/service binding。
[總管] 覆核
5fa3b79通過 ✅normalizeResource(scheme/host 小寫、去預設 port、去尾斜線)+等價形接受、存 canonical aud——你的技術裁決正確,比我 review 字面寫的「拒絕等價形」更對:拒等價形只是把「OAuth 成功但 /mcp 401」換位重現。採納你的版本。/authorizeredirectinvalid_target(redirect_uri 先驗)、/token400、aud一律 canonical 與 partner-auth 嚴格比對恆一致。vitest52/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 能連)。