CodeGym /課程 /ChatGPT Apps /MCP 授權架構:MCP Client、MCP Server、MCP Auth Server

MCP 授權架構:MCP Client、MCP Server、MCP Auth Server

ChatGPT Apps
等級 10 , 課堂 1
開放

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 ClientMCP ServerMCP 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(簽章、expaudscope),若無誤就執行商業邏輯。

關鍵點: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/tokenjwks_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 時必須用作 audienceresource
  • 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;
  • 提交 codecode_verifier(與上一步的 code_challenge 對應)。

Auth Server 會檢查 PKCE:把 code_verifier 雜湊後,比對原先的 code_challenge。若無誤且確為同一客戶端:

  • 簽發短效的 access_token(通常是 JWT);
  • 其中包含:
    • sub —— 使用者在 Auth Server 的 ID;
    • audresource —— 你的 MCP 伺服器;
    • scope —— 已允許的操作(gifts.readopenid 等)。

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 內部知道自己的使用者:它有 useremailid,可能還有 tenantroles。登入成功後,它會把這些資訊放進 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,並從中取出 userIdscopestenant
  • 實作商業工具(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 的授權目標。

錯誤四:忽略 audienceresource
如果 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 只是第一步;第二步永遠是正確地在商業程式碼中使用 userIdscope

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION