FastMCP 4 認證:從一把 token 到完整 OAuth 2.1
第 3 課的茶飲店誰都能連。放到公網、或者工具會動到真的資料,你需要三件事:知道是誰在呼叫、決定他能做什麼、 讓 Claude Desktop/Cursor 這類客戶端自己完成登入。FastMCP 4 把三層都做成 auth= 一行。 先感受一下「同一個工具、四把 token」——
上面每一列都是 notebook 的實測紀錄:它在 molab 裡真的起伺服器、真的簽 JWT、真的跑 OAuth,不連任何外部服務、不需要任何帳號。 本課與第 3 課同版本:fastmcp==4.0.0b1。
一把 token,三個概念:誰、能做什麼、掛在哪
StaticTokenVerifier 只適合開發教學(token 明文),但它把認證攤得最清楚: 不帶 token 敲門 → 401 + WWW-Authenticate: Bearer;帶一把亂掰的 → 同樣 401、只說 invalid_token, 不會告訴你是不存在還是過期(真正原因只進伺服器 log)。工具裡隨時 get_access_token() 就拿得到 client_id/scopes/claims——個人化與稽核的起點。
到 notebook 的 1️⃣–2️⃣ 節:401 長什麼樣、工具裡知道你是誰沒權限的工具不是 403,是隱形
| token | list_tools() | call_tool("close_shop") |
|---|---|---|
| alice(read write admin) | close_shop, remember, whoami | ✅ 已打烊 |
| guest(read) | remember, whoami | 🛑 Unknown tool: 'close_shop' |
對沒權限的人來說那個工具不存在——模型看不到就不會一直撞 403。多個 scope 是 AND;要改成「明確拒絕並說缺哪個 scope」用伺服器層的 AuthMiddleware(4.0 的 InsufficientScopeError,挑戰 LEVEL 2)。 第 3 課留的伏筆也在這裡兌現:session: UserSession 自動注入、不進 schema、不用傳任何鑰匙, alice 記兩件、guest 記一件,各拿各的;沒有身分時 FastMCP 明確報錯而不是默默開匿名桶。
到 notebook 的 3️⃣–4️⃣ 節:隱形的工具、每人一個桶簽發與驗章分開:伺服器只拿公鑰
JWT 三段 base64:header(RS256)、payload(sub/iss/aud/exp/scope, 不是加密,任何人都能解開看)、簽章(沒私鑰做不出來)。三種壞 token——audience 給別的 app、簽出來就過期、別組金鑰偽造的 admin—— 線路上長得一模一樣:401 invalid_token。issuer/audience 是兩道必檢:同一家 SSO 發給別的 app 的 token 在這裡無效。
到 notebook 的 5️⃣ 節:拆開 JWT、三種壞 token六步走完授權碼流程,每一步都是裸 HTTP
真實的 MCP 客戶端期待的是:連上去被拒 → 自己找到授權伺服器 → 自己註冊 → 跳瀏覽器讓使用者同意 → 自己換 token。 notebook 用 FastMCP 內建的 InMemoryOAuthProvider(完整的 OAuth 2.1 授權伺服器,只是自動按下「同意」)跟 MCP 伺服器掛同一個 port,然後用 httpx 一步一步走:
- POST /mcp 不帶 token → 401,WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp"——告訴你去哪裡問
- GET /.well-known/oauth-protected-resource/mcp → 這個資源信任哪些授權伺服器、支援哪些 scope
- GET /.well-known/oauth-authorization-server → authorization_endpoint/token_endpoint/registration_endpoint、PKCE 支援 S256
- POST /register(動態註冊 DCR,RFC 7591)→ 當場拿 client_id;公開客戶端沒有 secret,安全交給 PKCE
- GET /authorize?code_challenge=SHA256(verifier)&state=… → 302 回 redirect_uri,query 帶一次性的 code 與原封不動的 state
- POST /token(code + code_verifier 原文)→ access_token、expires_in、refresh_token
- POST /mcp 帶 Bearer → ✅。反例:新 code 配錯的 verifier → invalid_grant: incorrect code_verifier;用過的 code 再換 → authorization code does not exist
SDK 三行版 Client(url, auth=OAuth())(或 auth="oauth")全包上面六步:discovery、註冊、開瀏覽器、本機收 callback、換 token、到期自動 refresh。 molab 沒瀏覽器,notebook 示範覆寫 redirect_handler/callback_handler 兩個方法無頭跑完——在你自己電腦上不用覆寫。
到 notebook 的 6️⃣–7️⃣ 節:六步裸 HTTP、SDK 三行版接真的供應商:只有 auth= 那一行不同
| 情境 | 用 |
|---|---|
| 開發、測試、教學 | StaticTokenVerifier/InMemoryOAuthProvider |
| 公司已有 SSO 發 JWT | JWTVerifier(jwks_uri=...) |
| 讓使用者用 GitHub/Google 登入 | GitHubProvider/GoogleProvider(OAuthProxy:對客戶端假裝支援 DCR、對上游用你的固定憑證) |
| 身分平台支援 DCR | AuthKitProvider 等(RemoteAuthProvider) |
| 互動+機器兩種客戶端並存 | MultiAuth(server=..., verifiers=[...]) |
| 自己當完整授權伺服器 | OAuthProvider 子類別——除非有不得不的理由 |
換你動手
加一把 bob-token(只有 write),重跑授權表:bob 看得到哪些工具?remember 呢?
把「隱形」改成「明確拒絕」:FastMCP(middleware=[AuthMiddleware(auth=require_scopes("read"))]) + 一個要 read 與 write 的工具,用三把 token 比較清單與錯誤訊息。
用 grant_type=refresh_token 換一組新 token:新的能用嗎?舊的還能用嗎?舊 refresh_token 再用一次會怎樣?
卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
茶飲店伺服器要加一個 refund(退款)工具,只有 admin 能用——而且你不希望模型看到它之後一直試著呼叫。該怎麼做?
元件層的 auth=require_scopes("admin") 讓工具對沒權限的人隱形:list_tools() 裡沒有、硬呼叫只得到 Unknown tool——模型看不到就不會一直撞。A 擋得住執行,但工具還留在清單上,模型會反覆嘗試,而且每個工具都得手寫一段檢查;B 是整台伺服器的門檻,沒 admin 的人連 whoami、remember 都被擋在門外——它適合「全站至少要某個 scope」,不是鎖單一工具;D 把授權寫死在「是誰」上,多一個店長就要改程式——能做什麼該用 scope 表達。
Q2 錯誤診斷
公司 SSO 簽的 JWT 在另一個內部 app 用得好好的,拿來連你的 MCP 伺服器(JWTVerifier(jwks_uri=...))卻一直被拒。最可能的原因是?
issuer/audience 是兩道必檢:同一家 SSO 發給別的 app 的 token,aud 對不上這台伺服器就是 401——notebook「三種壞 token」的第一種。而且線路上一律只說 invalid_token,真正原因(audience mismatch)只寫進伺服器 log——除錯別盯著 401 猜,去看 log。A:JWT 前兩段只是 base64 編碼、不是加密,驗章用的是公鑰;C 症狀一樣,但跟「在別的 app 還用得好好的」矛盾——沒過期;D:JWT 的重點正是伺服器不用逐 token 連線查詢,jwks_uri 只是去拿公鑰。
Q3 錯誤診斷
你照 notebook 手走授權碼流程,換 token 的 cell 不小心重跑了一次,得到這個錯。code 是剛從 /authorize 的 302 撿到的、沒抄錯。最可能的原因是?
授權碼用過即銷毀——這是 OAuth 2.1 的一次性保證:就算 code 在跳轉途中被偷看,也只有先用掉的人換得到 token。重跑換 token 的 cell 就是拿用過的 code 再換一次,正是 notebook 反例表的那一列。修法:回 /authorize 重新要一個新 code(連同新的 code_verifier)再換。A 的錯誤訊息不同——verifier 錯會回 incorrect code_verifier;B 在註冊或授權那幾步就會先報錯,走不到換 token;C 症狀相同但時間對不上——code 幾分鐘內有效,剛撿到的不會過期。
Q4 情境題
你寫了一個每天凌晨自動執行的報表排程,要呼叫公司的 MCP 伺服器拉資料——凌晨沒有人在鍵盤前按「同意」。客戶端該怎麼認證?
沒有人在鍵盤前的客戶端走 client credentials:拿固定的 client_id/secret 直接向授權伺服器換 token,不開瀏覽器、不跳轉,到期再換一把就好。B 能動一陣子,但整條鏈建立在「第一次有人按同意」上——refresh token 一旦失效(被撤銷、rotation 斷鏈、伺服器重置),凌晨三點沒有人救得了它;C 是定時炸彈,expires_in 一到就開始 401;D 要回頭動伺服器端、token 還是明文——課裡說得直白:StaticTokenVerifier 只適合開發與教學。
實作在 molab 跑(免費)
molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。
- 登入 molab(GitHub / Google)
- 開啟課程 notebook,Fork 成自己的副本即可編輯
- 從第一格往下全部執行(首次安裝套件約 1 分鐘)——免費 CPU 環境即可,不需要 GPU、不需要任何外部帳號
不想用 molab?下載 fastmcp4-auth_ext.py 後在自己電腦
uvx marimo edit --sandbox fastmcp4-auth_ext.py,依賴會自動安裝。