1. 本講座涵蓋內容與不涵蓋內容
這會是一堂相當有趣的課,我們將會:
- 在腦中建立「信任三角形」的全貌:MCP Client、MCP Server 與 MCP Auth Server——以及站在這個三角形「之上」的使用者,他是資源擁有者;
- 講清楚 flow:誰把 token 傳給誰、使用者在哪裡登入,以及為什麼 MCP 伺服器永遠不會看到他的密碼;
- 把這一切和我們的 Next.js/MCP 後端以及未來的 Keycloak/Auth0 設定串起來。
我們今天不會做的事:
- 不會去 Keycloak 勾選設定,也不會配置特定 IdP;
- 不會寫完整的 JWT 驗證或 introspection——這些留到後面的課(關於 Auth Server 與作為受保護資源的 MCP Server)。
現在的任務——讓你可以拿一張紙,在 ChatGPT、你的伺服器與 Auth0/Keycloak 之間畫出箭頭,並且不結巴地解釋:哪裡登入、哪裡拿 token、哪裡取資料。
2. 信任三角形:MCP Client、MCP Server、MCP Auth Server
先從角色開始。技術上的「信任三角形」由 MCP Client、MCP Server 與 MCP Auth Server 構成;使用者(User)是獨立的角色——資源的擁有者——站在這個三角形之上並授權存取。在 MCP 與 Apps SDK 的語境中,這個架構被相當清楚地形式化。
User (Resource Owner)
就是螢幕另一端的人。他會:
- 進入 ChatGPT;
- 發出請求「顯示我的訂單/我的禮物清單」;
- 同意將你的服務帳號「連結」到 ChatGPT。
重點:資源(訂單歷史、個人資料、禮物清單)的擁有者是他;而授權存取這些資源的也是他。
MCP Client
在這裡對我們而言是:
- ChatGPT 搭配 Apps SDK;
- 有時候是 MCP Jam Inspector(除錯時)。
MCP Client 會:
- 讀取你 MCP 伺服器的中繼資料(透過 .well-known);
- 在使用者的瀏覽器啟動 OAuth flow;
- 保存並在呼叫 MCP 工具時附上 token。
要記住 MCP Client 是public client。它不會保存你的 client_secret,因此會像公開的 SPA 應用那樣與 Auth Server 互動:Authorization Code + PKCE。
MCP Server (Resource Server)
這是你的實作 MCP 的後端:
- 與 ChatGPT 建立連線;
- 宣告工具(tools)、資源、prompts;
- 對每一次工具呼叫檢查標頭 Authorization: Bearer <token>;
- 驗證 token(簽章、exp、aud、scope),若無誤就執行商業邏輯。
關鍵點:MCP 伺服器不負責登入。它不會看到密碼、不會畫出登入表單、不會寄「請確認 email」的信給使用者。它只信任由 Auth Server 加密簽署過的 token。
MCP Auth Server (Authorization Server / IdP)
這是獨立的身分驗證與授權服務:Keycloak、Auth0、Ory Hydra+Kratos、Okta、Cognito、Azure AD 等。
它負責:
- 登入 UI(email/密碼、SSO、2FA);
- 保存使用者帳號;
- 簽發 token(access token、refresh token);
- 發布 OAuth/OIDC 中繼資料(/authorize、/token、jwks_uri、/registration 等)。
對 MCP 而言,它需要支援 public clients 的 OAuth 2.1(PKCE S256、動態客戶端註冊等)。
角色總覽表
| 誰 | 職責 | 不負責 |
|---|---|---|
| User | 輸入登入/密碼,對資料存取給予同意 | 不會直接與 MCP Server 溝通 |
| MCP Client (ChatGPT/Jam) | 啟動 OAuth、保存 token、呼叫 MCP tools | 不驗密碼、不驗 token 簽章 |
| MCP Server | 驗證 tokens、執行 tools 的商業邏輯 | 不畫登入表單、不保存密碼 |
| MCP Auth Server | 讓使用者登入、簽發 tokens | 不瞭解你的 MCP 工具及其商業邏輯 |
如果你腦中一直把這些混成一個「什麼都做的大伺服器」——是時候分開了。
3. Flow 長什麼樣:從「沒有 token」到受保護的工具呼叫
現在看看訊息流程。在 MCP 規格中,這個過程被稱為「The Flow」:discovery → redirect → code → token → authorized calls。
步驟 0:嘗試在沒有 token 的情況下呼叫受保護的工具
使用者輸入:「顯示我儲存的禮物點子」。
作為 MCP Client 的 ChatGPT 判斷:「需要呼叫我們 MCP 伺服器的 getUserGiftLists 工具。」由於使用者尚未登入,它會在沒有 token 的情況下發出呼叫。
你的 MCP 伺服器會:
- 發現 Authorization 標頭缺失或不正確;
- 回應 401 Unauthorized,並加上標頭 WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" 提供受保護資源的中繼資料(resource metadata,見下文)。
大致如下(示意,非完整 HTTP):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"
ChatGPT 看到這個標頭就懂了:「啊哈,這個資源受 OAuth 保護,需要跑一個 OAuth flow 並連結帳號。」
Discovery:.well-known/oauth-protected-resource
MCP Client 接著向你的伺服器請求中繼資料:
GET /.well-known/oauth-protected-resource
伺服器回傳一個 JSON 文件,包含資源識別與可簽發 token 的授權伺服器列表。
最小示例(細節稍後配置,此處先理解概念):
{
"resource": "https://api.giftgenius.com",
"authorization_servers": [
"https://auth.giftgenius.com"
],
"scopes_supported": ["gifts.read", "gifts.write"]
}
其中:
- resource —— 你的資源的正規 ID;之後簽發 token 時必須用作 audience 或 resource;
- authorization_servers —— ChatGPT 可以向其申請 token 的 Auth Server 清單;
- scopes_supported —— 你的 MCP 伺服器能理解的「權限」。
Authorization Request:導向到 Auth Server
拿到中繼資料後,MCP Client 前往 Auth Server。它會在瀏覽器開啟一個分頁:
GET https://auth.giftgenius.com/authorize
?response_type=code
&client_id=chatgpt-giftgenius
&redirect_uri=... (MCP Client 的回呼 URL)
&code_challenge=...
&code_challenge_method=S256
&scope=openid gifts.read
&resource=https://api.giftgenius.com
使用者會:
- 看到熟悉的登入頁(例如 Keycloak 或 Auth0);
- 輸入登入/密碼,通過 2FA;
- 確認 ChatGPT 可以讀取他的禮物清單(scope gifts.read)。
Code → Token:用 PKCE 以 code 交換 token
登入成功後,Auth Server 會帶著 code 導回 MCP Client。接著 MCP Client:
- 對 /token 發送 POST;
- 提交 code 與 code_verifier(與上一步的 code_challenge 對應)。
Auth Server 會檢查 PKCE:把 code_verifier 雜湊後,比對原先的 code_challenge。若無誤且確為同一客戶端:
- 簽發短效的 access_token(通常是 JWT);
- 其中包含:
- sub —— 使用者在 Auth Server 的 ID;
- aud 或 resource —— 你的 MCP 伺服器;
- scope —— 已允許的操作(gifts.read、openid 等)。
Authenticated Request:夾帶 token 呼叫 MCP 工具
此時 MCP Client 準備好再次呼叫你的工具,不過這次會帶上標頭:
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
MCP 伺服器會:
- 驗證 token 簽章(用 Auth Server 的 JWK)或透過 introspection;
- 檢查有效期限(exp);
- 檢查 aud / resource——token 是否確實簽發給 https://api.giftgenius.com;
- 查看 scope,決定是否可呼叫 getUserGiftLists。
之後它就能依據某個 userId 進你的資料庫,回傳使用者的個人禮物清單。
注意,到此為止我們只討論網路 flow:token 如何取得並送達 MCP 伺服器。接著要理解的是,如何把 token 中的 sub 與其他 claims 轉成你資料庫中的 userId——這時就輪到 identity bridge 登場。
4. Identity Bridge:如何把 ChatGPT 的 user 變成你資料庫中的 userId
架構中最有意思的一塊就是「身分橋」(identity bridge)。MCP 規格明確強調:MCP 伺服器不認識 ChatGPT 的使用者,它只依賴 Auth Server token 裡的資料。
大致的圖如下:
flowchart TD User[User 在 ChatGPT] -->|Login/SSO| Auth[Auth Server] Auth -->|JWT: sub, email, tenant| MCP[MCP Server] MCP -->|userId/tenantId| DB[(你的資料庫)]
逐步來看如下。
首先,Auth Server 內部知道自己的使用者:它有 user、email、id,可能還有 tenant、roles。登入成功後,它會把這些資訊放進 token(claims)裡:
{
"sub": "auth0|abc123",
"email": "user@example.com",
"given_name": "Alice",
"https://giftgenius.com/tenant": "tenant-42",
"scope": "openid gifts.read",
"aud": "https://api.giftgenius.com"
}
其次,MCP Server 在驗證 token 後,會取出這些 claims 並決定這個人在它的世界裡是誰。例如:
- 如果 sub 已存在於表 User.authProviderId——就取對應的 userId;
- 如果不存在——就動態建立一筆本地紀錄(on‑the‑fly provisioning)並綁定。
MCP 伺服器端典型的 TypeScript 片段(簡化,未含簽章驗證)可能如下:
type TokenClaims = {
sub: string;
email?: string;
scope?: string;
};
async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
const user = await db.user.findUnique({ where: { authSub: claims.sub } });
if (user) return user.id;
const created = await db.user.create({
data: { authSub: claims.sub, email: claims.email ?? null }
});
return created.id;
}
第三,透過自己的 userId,MCP 伺服器就能取到所需的一切:禮物清單、訂單歷史、設定、方案等。
因此,Auth Server 成為外部世界(ChatGPT、Google、SSO)與你內部世界(訂單資料庫中的 customer_id)之間的「橋」。
5. 為什麼要把 Auth Server 與 MCP Server 分離
你可能會想:「乾脆讓我的 MCP 伺服器同時顯示登入、也自己簽發 token。」形式上做得到(把一個迷你 IdP 內嵌進去),但從架構上看並不好。原因很實際。
首先是安全與可擴展性。Auth Server 是一台重量級機器:2FA、社群登入、密碼政策、帳號鎖定、找回存取、登入稽核,甚至合規認證。若在每個微服務(每個 MCP 伺服器)重頭寫一次——那是自找麻煩與 PCI‑DSS 風險。把這些交給 Keycloak/Auth0,只要驗他們的 token 就好,輕鬆多了。
其次是客戶端可替換性。今天你只有 ChatGPT。明天你可能接入 Claude Desktop、自家的 Next.js 網頁前端、行動 App。它們都可以使用同一個 Auth Server 與相同的 OAuth 2.1 流程,而你的 MCP 伺服器只要持續驗 token 即可。不必為每個新客戶端重寫商業邏輯。
第三是程式碼乾淨。理想中的 MCP Server:
- 能發布 /.well-known/oauth-protected-resource;
- 能驗 Bearer token,並從中取出 userId、scopes、tenant;
- 實作商業工具(orders、gifts、profiles)。
所有登入 UI——表單、版面、社群登入——都在 Auth Server,避免汙染後端。
6. 這在我們的教學應用 GiftGenius 中長什麼樣
回到課程中一路打造的應用。假設我們有:
- 名為「GiftGenius」的 ChatGPT App(Apps SDK 小工具),能幫你挑選禮物;
- 一個 Node/Next.js 的 MCP 伺服器,提供工具:
- searchGifts —— 匿名,無需登入;
- getSavedGiftLists —— 個人化,需要驗證;
- Auth Server(稍後用 Keycloak/Auth0),每位使用者都有帳號。
匿名與已登入使用者的情境
如果使用者只說「幫我找給兄弟的禮物,他 30 歲,喜歡桌遊」,我們的 App 可以:
- 呼叫匿名工具 searchGifts;
- 在介面中給出建議。
在此情境下:
- 不需要 token;
- MCP 伺服器直接執行查詢(例如你的產品目錄或第三方 API)。
一旦使用者說「把這些存到我的清單」或「顯示我儲存的點子」,模型會決定呼叫受保護的工具 getSavedGiftLists。伺服器回 401 + WWW-Authenticate 並帶上 resource_metadata。ChatGPT 啟動 OAuth 精靈「Link GiftGenius account」,帶使用者完成登入並取得 token。
之後每一次受保護的呼叫:
- MCP Server 都會看到 Authorization: Bearer ...;
- 從 token 取出 userId;
- 依此 userId 篩選資料。
多虧如此,我們可以:
- 隔離不同使用者的資料;
- 安全地顯示訂單歷史、收藏清單;
- 實作 commerce 功能(課程稍後)。
後端架構:middleware + 工具處理器
在 Node/Next.js 的實作中,常見模式是「middleware 驗證 → 工具的商業處理器」。在講解 tool 處理器的課中,我們已強調要傳遞情境(context):user_id、tokens、設定。
程式片段可能如下:
// auth-context.ts
export type AuthContext = {
userId: string | null; // 匿名呼叫時為 null
scopes: string[];
};
掛在所有 MCP 端點上的 Middleware:
// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
const header = req.headers.authorization || "";
const token = header.replace(/^Bearer\s+/i, "");
if (!token) return { userId: null, scopes: [] }; // 匿名使用者
const claims = await verifyAndDecodeToken(token); // Token 驗證
const userId = await mapClaimsToUserId(claims);
const scopes = (claims.scope || "").split(" ");
return { userId, scopes };
}
工具處理器拿到這個情境:
// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
if (!ctx.userId) throw new Error("User must be authenticated");
return db.giftList.findMany({
where: { ownerId: ctx.userId }
});
}
重點在於,tool 處理器對 OAuth、PKCE 一無所知。它只處理「顯而易見」的 userId。所有 OAuth 的魔法都藏在它之前:MCP Client 與 Auth middleware。
7. 視覺化圖解:Client、Server 與 Auth 如何共存
我們已在第 3 節用文字走過流程。有時候畫一張圖比說七次更有用,下面用兩張圖表示相同的互動。
互動骨架(信任三角形)
flowchart TD U[User] -->|1. Login / Consent| A[MCP Auth Server] U -->|2. 對話| C["MCP Client (ChatGPT)"] C -->|3. OAuth Flow| A C -->|4. Bearer Token| S[MCP Server] S -->|5. Data| C
閱讀方式如下。
先由使用者透過 Auth Server 登入,Auth Server 確認其身分並簽發 token。MCP Client 管理整個流程,之後用 token 來呼叫 MCP 伺服器。MCP 伺服器看不到登入/密碼,它只看到 token,並據此決定允許的操作。
從請求到回應的訊息流
sequenceDiagram participant User participant ChatGPT as MCP Client participant Auth as Auth Server participant MCP as MCP Server User->>ChatGPT: "顯示我的禮物清單" ChatGPT->>MCP: callTool(getSavedGiftLists) (無 token) MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata) ChatGPT->>Auth: /authorize + PKCE User->>Auth: 輸入登入/密碼,給予同意 Auth-->>ChatGPT: redirect + code ChatGPT->>Auth: /token + code_verifier Auth-->>ChatGPT: access_token (JWT) ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ... MCP-->>ChatGPT: 包含個人清單的 JSON ChatGPT-->>User: 在小工具中渲染出的清單
這張圖就是你在課程結束時要能「閉著眼睛也能說」的流程。
8. 再深入一些:多資源、多客戶端、DCR
這種架構的好處——能擴展。
首先,你可以有多個 MCP 伺服器(例如一個負責禮物,一個負責訂單),共用同一個 Auth Server,由它簽發帶有不同 aud/resource 的 tokens。每個資源伺服器都必須檢查 token 確實是簽給自己的,否則就會出現經典的「confused deputy」問題:給 A 服務的 token 被 B 服務接受。
其次,你可以有很多客戶端:
- ChatGPT App;
- 你自己的前端;
- 行動應用;
- 透過 MCP Gateway 的合作夥伴整合。
它們都會:
- 讀取 /.well-known/oauth-protected-resource;
- 得知 Auth Server 在哪裡;
- 跑 OAuth 2.1 flow;
- 取得 tokens 並呼叫 MCP 伺服器。
第三,現代的 Auth Server 越來越常支援Dynamic Client Registration (DCR)——透過 API 動態註冊客戶端。MCP 規格正是預期這項能力:客戶端(ChatGPT/Jam)可以依據它的 registration_endpoint 自動在 Auth Server 註冊自己。
在本模組中,我們需要理解:
- MCP Client、MCP Server 與 Auth Server 透過標準化的 discovery 文件與 token 互動;
- 你不需要在後端程式碼中把所有客戶端「硬寫死」;
- 你可以擴充生態系,而不破壞既有的授權模型。
9. MCP 授權架構中的常見誤解
錯誤一:「MCP 伺服器應該自行讓使用者登入」。
有時開發者想把登入表單直接塞到 MCP 伺服器,然後透過工具傳送登入/密碼。這違背了 OAuth 的核心理念。MCP 伺服器在任何情況下都不該看到密碼。登入與同意是 Auth Server 的職責。MCP 伺服器只處理 token 與其中的 claims。
錯誤二:混淆 MCP Client 與 MCP Server。
有人把 ChatGPT 當成「我後端的一部分」,試圖在其中保存機密,或期待它自行檢查存取權限。其實 MCP Client 只是啟動 OAuth 並附上 token。驗 token 與權限是 MCP 伺服器的工作,不是 ChatGPT 的。
錯誤三:「用 .env 的 API 金鑰代替 OAuth」。
典型的反模式:做一個大的 SERVICE_API_KEY,把它放進 MCP 伺服器的 .env,以為萬事 OK。這樣就沒有使用者層級的權限分離,不能安全地顯示個人資料或進行購買,一切都「以服務的身分」執行,而不是以使用者為主。這完全違反 ChatGPT Apps 的授權目標。
錯誤四:忽略 audience 與 resource。
如果 MCP 伺服器接受任何簽章正確的 JWT,而不檢查 aud/resource,那麼相同 Auth Server 為其他服務簽發的 token 也可能被用來呼叫你的工具。這直接違反 OAuth 的安全模型。伺服器必須檢查 token 是否簽發給自己的 resource。
錯誤五:把授權邏輯與商業邏輯混在一起。
有時會把整個 token 解析、簽章檢查、JWK 操作等全部塞進 tool 處理器,導致程式碼脆弱難維護。更好的做法是把「token 檢查、映射為 userId」這一層(middleware)與「工具的實際邏輯」分離,後者只接收清楚的 AuthContext。
錯誤六:期待 ChatGPT 在沒有 .well-known 的情況下「自動搞定」。
沒有正確的 /.well-known/oauth-protected-resource 端點,MCP 客戶端根本不知道你的 Auth Server 在哪裡、需要哪些 scopes。結果就是:聊天介面「看起來」沒有登入能力,而開發者盯著空空的日誌困惑不已。正確做法:MCP 伺服器用 .well-known 明確宣告授權需求,客戶端讀取後才能建立 flow。
錯誤七:在商業邏輯中忘了使用者篩選。
有時即便正確設定了 OAuth 並把 token 映射為 userId,開發者仍然沒有在資料庫查詢中使用它:例如忘了以 ownerId = userId 過濾。這樣任何已授權的使用者都可能看到別人的資料。拿到 token 只是第一步;第二步永遠是正確地在商業程式碼中使用 userId 與 scope。
GO TO FULL VERSION