教程
MCP 客戶端如何釘死 OAuth 登錄方?Python SDK 1.30 與 2.2 升級後還要寫上 issuer
Server 端接上 OAuth 只解決「誰可以調工具」。9 月 28 日這則公告問的是客戶端:發現結果指過來的登錄方,你認不認。
2026 年 9 月 28 日,官方 MCP Python SDK 的維護者確認:有缺陷的 HTTP 客戶端可能把本該交給真實登錄方的 client secret、authorization code 和 PKCE 證明,送到對方控制的 token 端點。修復版是 1.x 的 1.30.0 和 2.x 的 2.2.0,9 月 7 日就已經發出,當時發行說明把它寫成行爲變更。公告本身落在 9 月 28 日,Cycode 同日公開了研究,次日多家安全媒體轉述。經過見 The Hacker News 的轉述,客戶端現在的正確接法見 OAuth clients。Server 怎麼要求 token,仍是站內的 MCP OAuth 2.1。你連接的是自己不完全掌控的遠程服務時,部署形狀見 Remote MCP 進生產。
公告修的是客戶端,不是你已經接好的 Server
受影響的是用官方 Python SDK 當 HTTP 客戶端、並且走 OAuth 的進程。它連上一個自己並不完全信任的 MCP Server,手裏又握着真實登錄服務的憑證。維護者的說明是:部分發現路徑沒有先確定期望的登錄方,就採用了 Server 指過來的那一方。修復版改成先算定期望的 issuer,再去取授權服務器元數據。元數據裏的 issuer 必須就是這一方,否則拒絕。存下來的憑證也會標上登錄方,標給 A 的記錄不會拿去跟 B 交換 token。
這和「給 MCP Server 加上 OAuth 2.1」不是同一層。Server 側決定誰能調用工具、令牌是不是發給這臺資源。客戶端側決定發現文檔指過來的授權服務器,是不是你當初發 client secret 的那一臺。兩層都要有。只做 Server,客戶端仍可能在發現階段跟錯對象。只升客戶端、Server 仍不校驗令牌受衆,也蓋不住另一類問題。
修復進入主線的說明在 PR 3398。授權服務器元數據必須自稱的 issuer 與獲取地址一致,依據是 RFC 8414 第 3.3 節。對不上就停,不要在客戶端里加「忽略 issuer」的開關。
| 你跑的是什麼 | 這則公告 | 升級之後還要做的 |
|---|---|---|
| HTTP 客戶端,OAuthClientProvider | 1.9.1–1.29.1,或 2.0.0–2.1.1 | 升到 1.30.0 或 2.2.0,清掉舊 client_info |
| ClientCredentials 或 PrivateKey JWT | 同樣的版本區間 | 升級之外必須傳入 issuer |
| SDK 寫的 Server、stdio、自帶 token | 不在這則公告的影響面 | 仍建議升到修復版,避免客戶端依賴舊包 |
先分清誰在影響面裏
1.x 從 1.9.1 到 1.29.1,2.x 從 2.0.0 到 2.1.1。用到的類包括 OAuthClientProvider、ClientCredentialsOAuthProvider、PrivateKeyJWTOAuthProvider,以及已經廢棄、沒有 issuer 參數的 RFC7523OAuthClientProvider。Cycode 將問題標爲高危。本地 stdio、自己在請求上附加 token、以及只用 SDK 來寫 Server 的部署,不在這則公告的影響面裏。
影響面窄,不代表倉庫可以留着舊 pin。同一個鎖文件裏,CLI、夜間作業和桌面客戶端經常共用 mcp 這一個依賴。你以爲只有 Server 在用 SDK,結果某個 HTTP 客戶端也裝了 1.29。升級按鎖文件裏的實際版本走,不按架構圖上的角色走。
舊版本上,公告給出的臨時辦法只有一條:開了 OAuth 的客戶端只連接你信任的 Server。已經連過不受信任的 Server,要在登錄方那裏輪換 client secret,並撤銷已發出的 token。這一步針對的是可能已經離開你環境的憑證,不是再去復現發現過程。
升到 1.30 或 2.2 之後,機器對機器仍要寫 issuer
瀏覽器流的 OAuthClientProvider 在修復版裏會自己先確定期望的登錄方,再取元數據。沒有瀏覽器的兩條路不一樣。ClientCredentialsOAuthProvider 和 PrivateKeyJWTOAuthProvider 要你傳入 issuer,值等於那臺授權服務器在 /.well-known/oauth-authorization-server 文檔裏聲明的 issuer。發現仍會跑,但換 token 只用這一方的元數據。Server 若指向別處,流程以 OAuthFlowError 停下。
不傳 issuer 時,這兩條路在修復版裏仍會跟隨發現結果。公告寫明:不加上這個參數,升級等於沒修到機器對機器。1.30.0 上的提醒是普通的 DeprecationWarning,Python 默認不顯示,CI 日誌裏經常看不見。3.0 會把它變成必填。現在就寫上,不要等大版本把作業跑紅。
RFC7523OAuthClientProvider 沒有 issuer 參數。繼續用它,升級也釘不住登錄方。遷到 ClientCredentialsOAuthProvider 或 PrivateKeyJWTOAuthProvider,再傳入 issuer。密鑰從環境變量或密鑰管理器讀,不要寫進倉庫。
你信任的那份元數據,issuer 必須等於獲取地址
先在你已經信任的授權服務器上取文檔,例如 https://auth.example.com/.well-known/oauth-authorization-server。文檔裏的 issuer 必須就是這臺服務器。比較是字符串相等,不做規範化:末尾斜槓、主機大小寫、www 前綴,都會變成另一個值。客戶端傳入的 issuer 用這份文檔裏的原文字符串,不要手寫一個「看起來一樣」的地址。
下面這份 JSON 是你信任的授權服務器元數據。issuer 與獲取它的源站一致,token 端點也在同一臺主機上。
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"code_challenge_methods_supported": ["S256"],
"authorization_response_iss_parameter_supported": true
}
Protected Resource Metadata 只負責告訴客戶端「哪一臺授權服務器在保護這臺 MCP」。它不是登錄方身份的替代品。客戶端要再去取那一臺的元數據,並覈對 issuer。資源元數據指向 A、授權服務器文檔自稱 B,就停。不要爲了讓集成先跑通,把 B 寫進 pin。
2026-07-28 修訂把動態註冊降級,推薦 Client ID Metadata Document:你在穩定的 HTTPS 地址發佈一份客戶端 JSON,這個 URL 就是 client_id。SDK 用 client_metadata_url 接入。授權服務器聲明支持時,不再向 /register 要一對新的 id 和 secret。URL 必須是帶非根路徑的 HTTPS,構造時就會檢查。已存的 client_info 仍然優先於這份文檔,所以舊記錄不刪,新釘的 issuer 不會生效。
清掉舊的 client_info,該輪換的密鑰分開處理
修復版會把登錄方標在存下來的註冊記錄上。升級前寫入的 client_info 沒有這道標記。公告要求清掉這些記錄,讓下一次註冊按新規則重新生成,並綁到正確的登錄方。只升包、留着舊文件,進程會繼續拿出一份沒有綁定的註冊。
清記錄和輪換密鑰是兩件事。清 client_info 是爲了讓新代碼重新註冊。輪換 client secret、在登錄方撤銷 token,是因爲你可能已經把憑證交給過不受信任的 Server。沒有這段歷史,就不必把每個內部客戶端的密鑰都換一遍。有這段歷史,就在登錄方操作,不要在聊天記錄裏貼 secret。
回調裏的 state 和 iss 要原樣交回 Provider。Provider 用 state 對上自己生成的值,用 iss 對上發現到的 issuer。這兩項是防混用的,不是調試字段。自己解析回調時丟掉 iss,等於拆掉修復版剛補上的一道覈對。
把釘死的 issuer 放進倉庫,當合同審
每個要連的 MCP 端點配一條記錄:Server URL,以及你允許的 issuer。值從授權服務器元數據原文抄來。評審看的是格式化之後的這份 JSON,不是某個開發者筆記本上的域名。兩次發佈之間做對比,看誰把 issuer 改到了另一臺主機。
倉庫裏留這份對照,不要留 client_secret。密鑰在密鑰管理器裏,這裏只釘登錄方。
{
"clients": [
{
"mcp_server": "https://orders.example.com/mcp",
"issuer": "https://auth.example.com"
}
]
}
機器對機器的構造參數和這份清單用同一個字符串。清單寫 https://auth.example.com,代碼裏多一個斜槓,修復版會當成另一方並停下。這是你要的失敗:作業紅在 OAuthFlowError,而不是悄悄換了一臺 token 端點。
提交前用JSON 格式化把元數據和 issuer 清單攤開,用JSON Schema 校驗確認 issuer、token_endpoint 都是字符串,再用JSON 對比看這次誰改了登錄方。
常見問題
只跑 stdio 的本地 Server,要不要處理這則公告?
stdio 客戶端不在影響面裏。同一個環境若還有 HTTP 客戶端共用這個包,按 HTTP 客戶端的版本升。鎖文件裏出現 1.9.1 到 1.29.1 或 2.0.0 到 2.1.1,就升到對應修復版,不要按「我們主要用 stdio」跳過。
升到 1.30.0 之後,日誌裏沒有警告,是不是已經安全?
不能這麼判斷。ClientCredentialsOAuthProvider 和 PrivateKeyJWTOAuthProvider 沒傳 issuer 時,提醒是默認隱藏的 DeprecationWarning。打開 Python 的 deprecation 警告,或直接在構造參數裏寫上 issuer。看不見警告不等於參數已經補上。
動態註冊和 Client ID Metadata Document 哪個更合適?
授權服務器已經聲明 client_id_metadata_document_supported 時,用一份放在 HTTPS 上的客戶端 JSON,URL 即 client_id,不再每次向 /register 申請 secret。對方還不支持時,SDK 會退回動態註冊。無論哪條路,已存的 client_info 都更優先,升級後要清掉舊記錄。
和 Server 側的 OAuth 2.1 文章怎麼配合?
Server 文章負責資源服務器如何要求 access token。這篇負責客戶端不要跟錯登錄方。遠程 Server 不在你自己的網關後面時,兩篇一起看:Server 校驗令牌,客戶端把 issuer 釘在構造參數和倉庫清單裏。
總結與下一步
9 月 28 日的公告把「發現結果說誰是登錄方,就信誰」從默認行爲裏拿掉了。HTTP 客戶端升到 1.30.0 或 2.2.0。機器對機器再傳入與元數據原文一致的 issuer。廢棄的 RFC7523 Provider 換掉。
然後清掉舊的 client_info。連過不受信任 Server 的,在登錄方輪換 secret 並撤銷 token。倉庫裏只保留 Server URL 和 issuer 的對照,用格式化、校驗和對比看住這個字符串。