AI 互動教室 ‹ 學 LLM 應用開發
下載 .py 開啟實戰 notebook ↗ 留言回報
FASTMCP 4 · 補充 A · AUTHENTICATION & AUTHORIZATION

FastMCP 4 認證:從一把 token 到完整 OAuth 2.1

第 3 課的茶飲店誰都能連。放到公網、或者工具會動到真的資料,你需要三件事:知道是誰在呼叫決定他能做什麼、 讓 Claude Desktop/Cursor 這類客戶端自己完成登入。FastMCP 4 把三層都做成 auth= 一行。 先感受一下「同一個工具、四把 token」——

上面每一列都是 notebook 的實測紀錄:它在 molab 裡真的起伺服器、真的簽 JWT、真的跑 OAuth,不連任何外部服務、不需要任何帳號。 本課與第 3 課同版本:fastmcp==4.0.0b1

01 · 認證

一把 token,三個概念:誰、能做什麼、掛在哪

from fastmcp.server.auth.providers.jwt import StaticTokenVerifier counter = FastMCP("會員櫃台", auth=StaticTokenVerifier(tokens={ "alice-token": {"client_id": "alice", "scopes": ["read", "write", "admin"]}, "guest-token": {"client_id": "guest", "scopes": ["read"]}, })) # 客戶端:給一個字串,FastMCP 自動加 Authorization: Bearer … async with Client(url, auth="alice-token") as c: ...

StaticTokenVerifier 只適合開發教學(token 明文),但它把認證攤得最清楚: 不帶 token 敲門 → 401WWW-Authenticate: Bearer;帶一把亂掰的 → 同樣 401、只說 invalid_token不會告訴你是不存在還是過期(真正原因只進伺服器 log)。工具裡隨時 get_access_token() 就拿得到 client_idscopesclaims——個人化與稽核的起點。

到 notebook 的 1️⃣–2️⃣ 節:401 長什麼樣、工具裡知道你是誰
02 · 授權

沒權限的工具不是 403,是隱形

@counter.tool(auth=require_scopes("admin")) def close_shop() -> str: return "已打烊" @counter.tool async def remember(fact: str, session: UserSession) -> list[str]: # 有身分 → 每人一個狀態桶 facts = await session.get("facts", default=[]); facts.append(fact) await session.set("facts", facts); return facts
tokenlist_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️⃣ 節:隱形的工具、每人一個桶
03 · JWT

簽發與驗章分開:伺服器只拿公鑰

sso = RSAKeyPair.generate() # 迷你 SSO:私鑰簽發(本課全程本機) auth = JWTVerifier(public_key=sso.public_key, # 正式環境:jwks_uri="https://你的SSO/.well-known/jwks.json" issuer="https://sso.example.com", audience="tea-shop") token = sso.create_token(subject="alice", issuer=..., audience="tea-shop", scopes=["read", "write"])

JWT 三段 base64:header(RS256)、payload(subissaudexpscope不是加密,任何人都能解開看)、簽章(沒私鑰做不出來)。三種壞 token——audience 給別的 app、簽出來就過期、別組金鑰偽造的 admin—— 線路上長得一模一樣:401 invalid_tokenissueraudience 是兩道必檢:同一家 SSO 發給別的 app 的 token 在這裡無效。

到 notebook 的 5️⃣ 節:拆開 JWT、三種壞 token
04 · OAUTH 2.1

六步走完授權碼流程,每一步都是裸 HTTP

真實的 MCP 客戶端期待的是:連上去被拒 → 自己找到授權伺服器 → 自己註冊 → 跳瀏覽器讓使用者同意 → 自己換 token。 notebook 用 FastMCP 內建的 InMemoryOAuthProvider(完整的 OAuth 2.1 授權伺服器,只是自動按下「同意」)跟 MCP 伺服器掛同一個 port,然後用 httpx 一步一步走:

  1. POST /mcp 不帶 token → 401WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp"——告訴你去哪裡問
  2. GET /.well-known/oauth-protected-resource/mcp → 這個資源信任哪些授權伺服器、支援哪些 scope
  3. GET /.well-known/oauth-authorization-serverauthorization_endpointtoken_endpointregistration_endpoint、PKCE 支援 S256
  4. POST /register(動態註冊 DCR,RFC 7591)→ 當場拿 client_id;公開客戶端沒有 secret,安全交給 PKCE
  5. GET /authorize?code_challenge=SHA256(verifier)&state=…302 回 redirect_uri,query 帶一次性的 code 與原封不動的 state
  6. POST /token(code + code_verifier 原文)→ access_tokenexpires_inrefresh_token
  7. 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_handlercallback_handler 兩個方法無頭跑完——在你自己電腦上不用覆寫。

到 notebook 的 6️⃣–7️⃣ 節:六步裸 HTTP、SDK 三行版
05 · 上線

接真的供應商:只有 auth= 那一行不同

# 沒有 DCR 的供應商(GitHub / Google / Azure / 多數企業 SSO):OAuthProxy 家族 auth = GitHubProvider(client_id=..., client_secret=..., base_url="https://your-server.example.com") # 有 DCR 的身分平台(WorkOS AuthKit / Descope / Keycloak):RemoteAuthProvider 家族 auth = AuthKitProvider(authkit_domain="https://your-project.authkit.app", base_url=...) # 沒有人在鍵盤前的客戶端(排程、CI、伺服器對伺服器):client credentials,不開瀏覽器 Client(url, auth=ClientCredentialsOAuthProvider(client_id=..., client_secret=..., scopes=[...]))
情境
開發、測試、教學StaticTokenVerifierInMemoryOAuthProvider
公司已有 SSO 發 JWTJWTVerifier(jwks_uri=...)
讓使用者用 GitHub/Google 登入GitHubProviderGoogleProviderOAuthProxy:對客戶端假裝支援 DCR、對上游用你的固定憑證)
身分平台支援 DCRAuthKitProvider 等(RemoteAuthProvider
互動+機器兩種客戶端並存MultiAuth(server=..., verifiers=[...])
自己當完整授權伺服器OAuthProvider 子類別——除非有不得不的理由
到 notebook 的 8️⃣ 節:決策表與各家 provider 的寫法
06 · 實戰

換你動手

LEVEL 1

加一把 bob-token(只有 write),重跑授權表:bob 看得到哪些工具?remember 呢?

LEVEL 2

把「隱形」改成「明確拒絕」:FastMCP(middleware=[AuthMiddleware(auth=require_scopes("read"))]) + 一個要 readwrite 的工具,用三把 token 比較清單與錯誤訊息。

LEVEL 3

grant_type=refresh_token 換一組新 token:新的能用嗎?舊的還能用嗎?舊 refresh_token 再用一次會怎樣?

卡住了?每一題在 notebook 末節都有折疊解答——先自己做,再打開對照。

07 · 驗收

情境測驗

離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。

Q1 情境題

茶飲店伺服器要加一個 refund(退款)工具,只有 admin 能用——而且你不希望模型看到它之後一直試著呼叫。該怎麼做?

元件層的 auth=require_scopes("admin") 讓工具對沒權限的人隱形:list_tools() 裡沒有、硬呼叫只得到 Unknown tool——模型看不到就不會一直撞。A 擋得住執行,但工具還留在清單上,模型會反覆嘗試,而且每個工具都得手寫一段檢查;B 是整台伺服器的門檻,沒 admin 的人連 whoamiremember 都被擋在門外——它適合「全站至少要某個 scope」,不是鎖單一工具;D 把授權寫死在「是誰」上,多一個店長就要改程式——能做什麼該用 scope 表達。

Q2 錯誤診斷

公司 SSO 簽的 JWT 在另一個內部 app 用得好好的,拿來連你的 MCP 伺服器(JWTVerifier(jwks_uri=...))卻一直被拒。最可能的原因是?

POST /mcp Authorization: Bearer eyJ… HTTP 401 WWW-Authenticate: Bearer error="invalid_token"

issueraudience 是兩道必檢:同一家 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 撿到的、沒抄錯。最可能的原因是?

POST /token (code + code_verifier 原文) ← invalid_grant: authorization code does not exist

授權碼用過即銷毀——這是 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 只適合開發與教學。

HANDS-ON · MOLAB

實作在 molab 跑(免費)

molab 的登入狀態進不了內嵌框架(瀏覽器的跨站 cookie 保護), 所以 notebook 要在新分頁執行——把它跟本頁並排開,左邊教學照樣對照。

  1. 登入 molab(GitHub / Google)
  2. 開啟課程 notebook,Fork 成自己的副本即可編輯
  3. 從第一格往下全部執行(首次安裝套件約 1 分鐘)——免費 CPU 環境即可,不需要 GPU、不需要任何外部帳號

不想用 molab?下載 fastmcp4-auth_ext.py 後在自己電腦 uvx marimo edit --sandbox fastmcp4-auth_ext.py,依賴會自動安裝。