Files
Arcrun/mcp/OAUTH.md
T
Claude 7d9d478baa feat(mcp): OAuth 2.1 server for claude.ai remote connector; close plaintext-namespace bearer hole
在 arcrun-mcp worker 實作 MCP Authorization 規範(OAuth 2.1 + PKCE S256),
讓 claude.ai 遠端 connector 安全登入;並修掉「Bearer 明碼 namespace 直接放行」漏洞。

安全模型
- /authorize 同意頁以 owner secret(CF Secrets MCP_OWNER_SECRET)把關,只有 owner 知道 →
  只知 URL 的人走不完 OAuth、拿不到 token。
- access_token 是 /mcp 唯一接受的 bearer(預設);明碼 namespace 舊路徑移除(步驟 5 直接 401)。

實作 endpoint(掛 worker 根路徑)
- RFC 9728 /.well-known/oauth-protected-resource(+/mcp 變體)+ 401 帶
  WWW-Authenticate: Bearer resource_metadata=...
- RFC 8414 /.well-known/oauth-authorization-server(response_types=code, S256, none)
- RFC 7591 /register(public client,無 secret,無狀態不落地)
- GET/POST /authorize(PKCE S256 + owner-secret 閘 + redirect_uri 白名單)
- POST /token(authorization_code + PKCE 驗證 → access_token 綁定 owner namespace)

儲存鐵律
- authorization code / access token → 短效 KV OAUTH_KV(key 用 SHA-256 hash、帶 TTL、code 一次性)
- owner secret / static token → CF Secrets(非 KV、非明碼 var)
- DCR client / refresh token → 不落地(無狀態 / 不實作,避免長效機密進 KV)

相容決策
- 本機 CLI/GUI/Claude Code → 用真祕密 MCP_STATIC_TOKEN(CF Secret)取代舊明碼 namespace
- 官方 SaaS partner-key 路徑行為不變
- ALLOW_PLAINTEXT_NAMESPACE 逃生門預設關(僅遷移期)

驗證:tsc exit 0;vitest 42/42(oauth 22 + partner-auth 10 改測真實 middleware + 既有 10);
wrangler deploy --dry-run 打包過、OAUTH_KV binding 正確識別。設計文件 mcp/OAUTH.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
2026-07-07 03:59:00 +00:00

129 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# arcrun-mcp OAuth 2.1 Serverclaude.ai 遠端 connector 安全登入)
> Design 文件。對應 PR `feat/mcp-oauth-server`。實作全在 `mcp/`(改既有 arcrun-mcp 這一顆 worker
> 的框架碼,非新 worker、非應用工作流、無 service binding)。
## 1. 為什麼(安全定調)
MCP 打進 arcrun = 觸及該租戶 **KBDB 全量讀寫**,是個資外泄面,必須有真認證。
**修掉的漏洞**`partner-auth.ts` 舊行為在 `MULTI_TENANT=false`self-hosted)時,把 `Authorization:
Bearer <x>``<x>` 直接當成 `org_namespace` 明碼放行。於是**任何知道 URL 的人送 `Bearer leo`
就能讀寫 leo 的全部資料**。這正是本次要關掉的洞。
**鐵律**:只知道「網址 + 明碼 namespace」的人,必須讀不到任何資料。
## 2. 安全模型(owner secret 怎麼把關)
claude.ai remote connector 走 **OAuth 2.1 + PKCES256**。整條鏈唯一的「人類祕密閘」在
`/authorize` 同意頁:要求輸入 **owner secret**`MCP_OWNER_SECRET`,存 CF Secrets,非 KV、非明碼 var)。
- 祕密**正確** → 才發 authorization code → claude.ai 用 PKCE `code_verifier``access_token`
- 祕密**錯誤 / 未帶** → 不發碼,重顯同意頁(401)。
- `MCP_OWNER_SECRET` **未設**`/authorize` 直接回 503(拒絕在無把關下發碼,不留不安全預設)。
**為何「只知 URL 的人」進不來**:他能打開 `/authorize` 頁、能自己跑 DCR 拿 `client_id`、能發起
PKCE,但**走到發碼那步需要 owner secret**,而 secret 只在 CF Secrets、只有 owner 知道。拿不到 code →
換不到 token → 打 `/mcp` 一律 401。access_token 是唯一被 `/mcp` 接受的 bearer(見 §5 相容決策)。
**縱深防禦**
- **PKCE S256 強制**`/authorize``/token` 都要求 `code_challenge_method=S256``plain`/缺省一律拒。
- **authorization code 一次性 + 極短 TTL600s)**:讀到即從 KV 刪,防重放。
- **redirect_uri 白名單**DCR 無狀態,見 §5):預設只允許 `claude.ai``claude.com``anthropic.com`
(含子網域)+ `localhost``http` 僅限本機。擋 open-redirect/釣魚把 code 送去攻擊者。可用
`MCP_ALLOWED_REDIRECT_HOSTS` 調整。
- **token audience 綁定(RFC 8707**codetoken 記 `resource``aud` = 本 MCP server 的 canonical URI。
- **KV key 用 SHA-256 hash**codetoken 不以明碼當 key,KV list 也拿不到可用憑證(比照
`wasi-shim.ts` 只寫短效 oauth2 cache 的精神)。
- **owner secretstatic token 用常數時間比對**:防 timing attack。
## 3. Endpoint 清單(全掛在 worker origin 根,非 `/mcp` basePath
| Method | Path | 規範 | 作用 |
|---|---|---|---|
| GET | `/.well-known/oauth-protected-resource`+`/mcp` 後綴變體) | RFC 9728 | Protected Resource Metadata`resource` + `authorization_servers` |
| GET | `/.well-known/oauth-authorization-server`+`/mcp` 後綴變體) | RFC 8414 | AS Metadataauthorize/token/registration endpoint、`S256``code``none` |
| POST | `/register` | RFC 7591 | Dynamic Client Registration → 回 `client_id`public client,無 secret |
| GET | `/authorize` | OAuth 2.1 | 呈現 owner-secret 同意頁(要求 `response_type=code` + PKCE S256 + 合法 redirect_uri |
| POST | `/authorize` | OAuth 2.1 | 驗 owner secret → 發 code → 302 redirect 回 `redirect_uri?code=&state=` |
| POST | `/token` | OAuth 2.1 | `authorization_code` + `code_verifier`(PKCE) → `access_token` |
**未帶有效 token 的 `/mcp`(及 GUI REST 端點)**:回 **401 + `WWW-Authenticate: Bearer
resource_metadata="<origin>/.well-known/oauth-protected-resource"`**RFC 9728 §5.1),claude.ai 靠這個
發現 OAuth。
metadata 以「當前請求 origin」動態生成 → 同一份碼在 `mcp.arcrun.dev``arcrun-mcp.<sub>.workers.dev`
都正確。
## 4. 各資料存哪(儲存鐵律逐項)
| 資料 | 存哪 | 理由 |
|---|---|---|
| **authorization code** | 短效 KV `OAUTH_KV`key=`oauth:code:<sha256>`,TTL 600s,一次性 | 「取得的暫時性認證」,允許進短效 KV |
| **access token** | 短效 KV `OAUTH_KV`key=`oauth:tok:<sha256>`TTL=`MCP_TOKEN_TTL`(預設 30 天) | 同上;過期自動消失 |
| **owner secret** | **CF Secrets** `MCP_OWNER_SECRET``wrangler secret put`) | 長效機密,非 KV、非明碼 var |
| **static token(相容用)** | **CF Secrets** `MCP_STATIC_TOKEN`(選配) | 長效機密,同上 |
| **DCR client 註冊** | **不落地(無狀態)** | public client 無 secret,非機密;不需持久 → 不塞 KV(守鐵律) |
| **refresh token** | **不實作**(見 §5) | 避免長效機密落地;改短效 access_token + 到期重新授權 |
| owner namespace / token TTL / redirect 白名單 | `[vars]`(非機密設定) | 純設定值 |
## 5. 相容決策(明碼-namespace-bearer 舊路徑怎麼處理)
**預設安全優先,遠端一律走 OAuth。** `partner-auth.ts` 新認證順序:
1. **OAuth access_token**`OAUTH_KV` 查得到)→ 解出綁定 namespace。**遠端 claude.ai 的正規路徑。**
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"`** → 恢復舊明碼路徑。**僅遷移期**,設了等於重開漏洞,正式勿用。
5. 皆不符 → 401 + `WWW-Authenticate`
**明碼 namespace 當 bearer 的舊路徑已從預設移除**(步驟 5 直接 401)。之所以保留步驟 2/4:
- 本機 Claude Code MCP 目前經 `acr mcp-setup``namespace` 寫進 `.mcp.json``Authorization`
header。若硬砍會斷本機整合。**正解=改用 `MCP_STATIC_TOKEN`(真祕密)**`mcp-setup` 寫入真祕密而非
明碼 namespace 屬 CLI 側後續(本 PR 未動 CLI,於報告標為待辦)。
- `ALLOW_PLAINTEXT_NAMESPACE` 只是遷移期的明確 opt-in 逃生門,預設關 = 預設安全。
**為何不做 refresh token**refresh token 需長效持久化,依鐵律得進 CF Secrets/KBDB 而非 KV,成本與
面積都大。改採「較長 TTL 的 access_token(預設 30 天)+ 到期重走 OAuth(owner 重輸祕密)」,兼顧安全
(週期性重認證)與簡潔(無長效機密落地)。TTL 由 `MCP_TOKEN_TTL` 調。
## 6. 測試涵蓋
`mcp/tests/unit/oauth.test.ts`22):
- 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` 格式。
- consent XSS escape(惡意 `state` 不注入)。
- 完整流程整合(Hono `app.request`):well-known 動態 origin、DCR 回 client_id 無 secret、redirect_uri
白名單擋非法 host、GET `/authorize` 要 PKCE、`MCP_OWNER_SECRET` 未設→503、**正確祕密+正確 verifier→
access_token**、**錯誤祕密→401 不發 code**、錯誤 verifier→invalid_grant、重用 code→invalid_grant、
`OAUTH_KV` 未設→503。
`mcp/tests/unit/partner-auth.test.ts`10,改測真實 middleware):
- 無/壞 Authorization → 401 + `WWW-Authenticate`RFC 9728)。
- OAuth token → 解出 namespace;未知 token(明碼)在 self-hosted → **401(洞已補)**
- `MCP_STATIC_TOKEN` 相容路徑通過 / 明碼被擋。
- 官方 SaaS partner-keymock KBDB)行為不變。
- `ALLOW_PLAINTEXT_NAMESPACE` 逃生門開/關。
全部 42 tests 綠;`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 的後續,見報告待辦)。
2. **設 owner secretCF Secrets**`wrangler secret put MCP_OWNER_SECRET`(輸入只有你知道的強祕密)。
3.(選配)**設本機相容 static token**`wrangler secret put MCP_STATIC_TOKEN`,並把本機 `.mcp.json`
`Authorization: Bearer <此值>`(取代舊明碼 namespace)。
4.(選配)`[vars]` 調 `MCP_OWNER_NAMESPACE`(預設 leo/`MCP_TOKEN_TTL`(預設 2592000/
`MCP_ALLOWED_REDIRECT_HOSTS`
5. **重部署 arcrun-mcp**(走 leo21c wrangler 直推,勿 `acr update`——codeload 陷阱 mistakes #23)。
6. 驗收:`curl <origin>/.well-known/oauth-protected-resource` → 200;未帶 token 打 `/mcp` → 401 帶
`WWW-Authenticate`claude.ai 加 remote connector 走完 OAuth(輸 owner secret)能連上。
> ⚠️ 若 KV/secret 未就緒:OAuth 端點誠實回 503、`/mcp` 回 401(不假綠);既有官方 SaaS partner-key
> 路徑不受影響。