iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
佛心分享-SideProject30

為你自己蓋一座會複利的知識庫——WikiBrain系列 第 20 篇

Day 20 - 讓 Claude.ai 走 OAuth 2.1 連進來

  • 分享至 

  • xImage
  •  

前言

Cursor 和 Claude Code 可以手貼 token。Claude.ai 和 ChatGPT 的網頁版不行,你只能給它一個網址,剩下的它要自己談。那個「自己談」的協定是 OAuth 2.1 加上動態用戶端註冊。

使用者看到的流程

使用者做的事只有四步:

  1. 在 Claude.ai 的設定裡新增一個自訂連接器,貼上 https://你的網域/mcp
  2. 跳出我的同意頁,如果沒登入就先登入
  3. 按「允許」
  4. 回到 Claude.ai,六個工具出現在工具清單裡

從此之後,Claude.ai 可以讀寫這座知識庫,隨時可以在我的設定頁撤銷。

OAuth 同意頁:顯示哪個用戶端要求授權、將被導回哪個網址
同意頁是整個流程裡唯一有人類判斷力的環節,所以要把對方是誰、會被導去哪裡寫清楚。

機器之間發生了什麼

第一步是探測。Claude.ai 打 /mcp,拿到 401,而且標頭裡有:

WWW-Authenticate: Bearer resource_metadata="https://你的網域/.well-known/oauth-protected-resource/mcp"

之前我們已經提過這個標頭,這裡就是它的用途。沒有它,流程在這裡就結束了。

第二步是動態註冊。Claude.ai 讀到那份 metadata,發現這台伺服器支援動態註冊,於是 POST 到 /register 說「我是一個叫 Claude 的用戶端,我的 redirect URI 是這些」。伺服器產生一組 client id 存起來。

這一步是 OAuth 少見的地方:一般 OAuth 要你先去開發者後台申請 client id,這裡是程式自己申請。因為 MCP 的情境是「任意的 client 連任意的伺服器」,人工申請不可能規模化。

第三步是授權。它把使用者導到 /authorize,帶著 client id、redirect URI、還有 PKCE 的挑戰碼。伺服器驗證這些參數,把請求存起來,然後把使用者導到我自己的同意頁。使用者按允許,伺服器發一個短期的授權碼,導回 Claude.ai。

![OAuth 的時序:Claude.ai 打 /mcp 拿到 401 與 resource_metadata,讀 metadata、動態註冊、導向 /authorize、使用者在同意頁按允許、最後用授權碼換 token]

整段流程裡使用者只做了兩件事:貼一個網址、按一次允許。

第四步是換 token。Claude.ai 拿授權碼加上 PKCE 的驗證碼打 /token,換到 access token 跟 refresh token:

const ACCESS_TTL_S = 24 * 3600;           // access token 24 hours
const REFRESH_TTL_S = 90 * 24 * 3600;     // refresh token 90 days

出處:src/oauth.ts:18-19

之後所有 MCP 請求都帶那個 access token,跟手貼的 token 走同一套驗證邏輯,存進同一張 mcp_tokens 表,只是多了 client_id 跟 refresh 的欄位。使用者在設定頁看到的那份連線清單,兩種來源混在一起列,撤銷的方式也一樣。

幾個必須做對的細節

授權碼要原子換用。一個授權碼只能換一次。實作上不能「先查詢,再刪除,再發 token」,那中間有窗口。要用一句 SQL 把標記跟取值綁在一起:

UPDATE oauth_requests SET code_used_at = now()
 WHERE code_hash = $1 AND client_id = $2 AND code_used_at IS NULL
   AND expires_at > now() AND user_id IS NOT NULL
RETURNING user_id, workspace_id, scopes, redirect_uri

出處:src/oauth.ts:94-95

跟之前講樂觀鎖時是同一招:條件寫進 WHERE,讓資料庫保證只有一個人會拿到那一列。同一個碼被同時送兩次,第二次更新到零列,回空的。

redirect URI 要精確比對。不能用前綴比對,不能允許萬用字元:https://good.com.evil.com 這種網址就是要混過草率的前綴檢查。這一段我沒有自己寫,MCP 官方 SDK 的授權路由已經幫你驗 client、redirect URI 跟 PKCE,我只負責把請求存起來、發碼、換 token。

PKCE 一律要求。現在的 OAuth 2.1 已經把它列為必要,別自己想例外。

scope 我一開始做錯了:寫進 metadata,但驗證的時候沒有檢查,那等於沒有。現在 token 帶著 scope,寫入類的工具會檢查有沒有 notes:write,refresh 換發時 scope 只能縮不能擴。

同意頁要顯示對方是誰。上面要寫清楚是哪個 client 在要求授權、它會被導回哪個網域。如果那個網域跟它自稱的官網不符,或者不是 https,要跳警告。

動態註冊代表任何人都能來註冊,所以要有上限:

if (JSON.stringify(client).length > 8 * 1024) throw new InvalidRequestError('client metadata too large (8 KB max)');
if ((client.client_name ?? '').length > 100) throw new InvalidRequestError('client_name too long (100 chars max)');
if (client.redirect_uris.length > 10) throw new InvalidRequestError('too many redirect_uris (10 max)');

出處:src/oauth.ts:30-32

加上一支每小時跑的清理:過期的授權請求隔一天刪掉,而註冊了三十天卻從來沒換過 token、也沒有待處理請求的 client 直接刪掉。

DELETE FROM oauth_clients c
 WHERE c.created_at < now() - interval '30 days'
   AND NOT EXISTS (SELECT 1 FROM mcp_tokens t WHERE t.client_id = c.client_id)
   AND NOT EXISTS (SELECT 1 FROM oauth_requests r WHERE r.client_id = c.client_id)

出處:src/oauth.ts:125

沒有這支清理,一個公開的註冊端點就是一張任何人都能寫的表。

小結

這段實作裡自己寫的部分不多:驗 client、redirect URI 與 PKCE 都是 SDK 做掉的。我寫的是四件:授權碼用一句 SQL 原子換用、scope 在每次呼叫真的檢查、同意頁把對方是誰跟導回哪裡寫清楚、公開的註冊端點加上限並且每小時清一次。


上一篇
Day 19 - 接上 better-auth,用 AES-256-GCM 存使用者的 API key
下一篇
Day 21 - 打包成 Docker 映像,部署上 Railway
系列文
為你自己蓋一座會複利的知識庫——WikiBrain 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言