1. 實務中的 Auth Server 是什麼,為什麼選 Keycloak
先快速複習一下:Auth Server(IdP)是一個服務,它:
- 向使用者顯示登入/註冊與同意(consent)畫面;
- 發出 OAuth/OIDC 權杖(access_token、id_token、refresh_token);
- 發布 discovery 文件與 JWKS 金鑰,讓資源伺服器可以驗證這些權杖。
在我們的堆疊中:
- ChatGPT / MCP Jam 充當 OAuth 客戶端(public client);
- 你的 MCP 伺服器 作為 Resource Server;
- Keycloak 作為 Auth Server。
為什麼 Keycloak 對課程與實務都很方便:
- 它是 open source,容易在本機或 Docker 起來;
- 它的實體模型相當清楚:realm、clients、users、roles;
- 基本上,你在 Keycloak 學到的設定,之後幾乎可以 1:1 移植到 Auth0/Okta/Cognito:觀念相同——client、scopes、redirect URIs、PKCE。
關鍵觀念:我們設定的不是「整個專案共用的 Keycloak」,而是為本次 ChatGPT App 建一個專用的 Realm。這就像是專門給 MCP 客戶端做驗證的「沙盒」。
總之,這堂課結束後你將擁有:
- 一個在 Keycloak 中為 ChatGPT 應用建立的 realm;
- 啟用 Authorization Code + PKCE 的 public client;
- 最小必要的 scopes 與 claims;
- 了解這個權杖如何在你的 Node MCP 伺服器中運作。
2. 以 MCP 視角了解 Keycloak 的基本實體
為了之後不在管理介面迷路,我們先把實體概念釐清。
Realm:設定與使用者的空間
在 Keycloak 中,Realm 是一個隔離的空間,擁有自己的一組使用者、客戶端與政策。 一個好比喻是「在商辦大樓承租的一間辦公室」:每個租戶都有自己的房間、員工名單與進出規則。
在課程與你的第一個實際 App 中,建議建立獨立的 realm,例如 giftgenius-mcp 或 mcp-course。這樣可以:
- 不去動 master realm,避免不小心弄壞管理介面;
- 透過匯出/匯入 realm,在不同環境(dev / staging / prod)之間重用設定與使用者。
Client:應用程式的登記(ChatGPT / MCP Jam)
在 Keycloak 中,Client 並非「使用者」,它是向 Auth Server 取用權杖的應用程式。 在我們的案例中,這不是你的 Next.js 後端,而是 MCP 客戶端: ChatGPT、MCP Jam,或者若你在 UI 中手動走 OAuth 流程,也可能是你的獨立小工具。
Client 的關鍵欄位:
- client_id——字串識別碼;
- 類型(public / confidential / bearer-only);
- 啟用的 OAuth 流程(Standard Flow、Client Credentials 等);
- 允許的 redirect URI 清單;
- scopes 與 protocol mappers(權杖中的 claims)。
對於 ChatGPT/MCP Jam,我們需要 public client,因為:
- 作為客戶端的 ChatGPT 無法安全保存 client_secret;
- MCP Jam 作為桌面/瀏覽器工具,也是在不可信環境中執行。
User:真實使用者
User 指的是「真人使用者」:他具有 username、密碼、email、屬性、群組、角色。 當有人透過 Keycloak 登入時,就是他的 sub 與其他資料會被放入權杖, 接著你會在 MCP 伺服器上驗證並對應到你自己的 accountId / tenantId。
在我們的示範層級,足夠的是:
- 一兩個測試使用者(例如 alice@example.com、bob@example.com);
- 也可以加幾個屬性,例如 tenant 或 plan,以示範權杖中的 claims 如何影響 tools 的行為。
3. 選擇客戶端類型:public、PKCE,以及為何不使用 secret
接下來是重點:如何在 Keycloak 中為 ChatGPT/MCP 設定客戶端。
Public vs Confidential:為何不用 client_secret
在傳統 Web 應用中,你會有後端,把 client_secret 放在那裡,由伺服器代表你去 IdP 取權杖。 這是 confidential client:它可以保存 secret。
在 ChatGPT 的世界中情況相反:
- OAuth 客戶端是 ChatGPT 平台本身或像 MCP Jam 這樣的工具;
- 你不控管它的程式碼與執行環境;
- 任何你交給 ChatGPT 的 client_secret,都必須視為立即曝光。
因此 ChatGPT/Jam 以 public clients 方式運作,也就是不使用 client_secret, 並用 PKCE(Proof Key for Code Exchange)來補強。
用白話解釋 PKCE 在做什麼
PKCE 就是一個「每個工作階段的一次性祕密」。它的目標是避免授權碼被攔截後,能在別處兌換成權杖。 流程如下:
- 客戶端產生一個隨機字串 code_verifier。
- 對它做雜湊(通常是 SHA-256)得到 code_challenge。
- 在導向至 /authorize 時一併送出 code_challenge 與 code_challenge_method=S256。
- 完成登入後,使用者會帶著 code 回到 redirect URI。
- 客戶端對 /token 發出 POST,同時傳遞 code 與原始的 code_verifier。
- Keycloak 會對 verifier 做雜湊並與 challenge 比對,若一致就簽發權杖。
對我們而言重要的是:這一切都由 ChatGPT/MCP Jam 代勞。 我們只需要在 Keycloak 的 client 中啟用 Authorization Code + PKCE(S256),且不要求 client_secret。
4. 逐步設定 Keycloak 以支援 MCP 情境
總結一下:對 ChatGPT/MCP Jam 而言,我們需要一個 public client, 啟用 Authorization Code Flow 與 PKCE(S256),且無 client_secret。 現在看看在 Keycloak 設定上該如何對應。
假設你已經有在運作的 Keycloak(Docker 容器或本機安裝都可)。 此處我們重視設定邏輯,而非實際要在哪點擊。
為 App 建立新的 realm
建立名為 giftgenius-mcp 的 realm:這是一個獨立區域,裡面會有:
- 專供 ChatGPT 應用使用者;
- ChatGPT/MCP Jam 用來走 OAuth 的客戶端;
- 自己的密碼與權杖政策。
實務建議:不要把你給後台員工使用的 realm 與 ChatGPT 客戶端的 realm 混在一起。 這樣更安全,也更容易維護邏輯。
加入測試使用者
建立一個使用者,例如 alice:
- username:alice;
- email:alice@example.com;
- 設定密碼(為了簡化——先不套用複雜政策);
- 可選地加入屬性 tenantId=demo-tenant 或角色 ROLE_PREMIUM。
之後在 MCP 伺服器中,你能解碼權杖,取出 sub、email、tenantId,並把它們連結到你的使用者模型。
為 MCP Jam / ChatGPT 建立 public client
接著是最有趣的部分——Client。
在概念層級,參數應該長這樣:
- Client ID:giftgenius-mcp-client(名稱可自訂);
- 類型:public / Client Authentication off;
- 啟用 Standard Flow(Authorization Code);
- 啟用 PKCE,方法為 S256;
- 設定 redirect URI;
- 設定所需 scopes(openid + 你的自訂項,例如 mcp:tools)。
啟用 Standard Flow 與 PKCE
在概念上:
- 啟用 Authorization Code Flow(常見的標籤叫「Standard Flow Enabled」);
- 在 PKCE 區塊設定 pkceRequired=true,並通常明確指定 code_challenge_method=S256。
為什麼選 S256:在現代 OAuth 2.1 文件以及 OpenAI/Model Context Protocol 的建議中,S256 被視為安全方法;plain-PKCE 被視為不安全。
Redirect URI——最脆弱的一環
Redirect URI 必須與客戶端實際使用的值逐字相同。 否則會在授權階段得到 invalid_redirect_uri 錯誤。
在本課程中有兩種典型客戶端:
- MCP Jam/Inspector 用於除錯。它們通常在 http://localhost:PORT/... 運作。對本機情境,合理允許如下 redirect:
- http://localhost:5173/* 或 Jam 使用的具體路徑。
- ChatGPT / Apps SDK 的正式環境。此處的 redirect URI 由平台本身決定。 在實際整合中,你需要查閱 OpenAI 的最新文件並填入 ChatGPT 當作 callback 的正確 URL。
在本講重點是理解:ChatGPT 不能隨便使用任何 redirect,它必須與 Auth Server 中登記的值一致。因此:
- 切勿填 * 或「任何 URL 都可」;
- 對本機開發,可在 localhost 範圍內允許萬用字元,但正式環境不可。
Scopes:最小但足夠
Scopes 是客戶端請求的權限清單。
在我們的 MCP 情境中,通常需要:
- openid——開啟 OpenID Connect,並取得帶有 sub(有時還有 email)的 id_token;
- 自訂 scope,例如 mcp:tools,代表「允許使用 MCP 工具」。
在 Keycloak 中可透過 Client Scopes 完成:
- 保留 openid;
- 預設關閉不需要的多餘 scopes,例如 profile 與 email;
- 新增 scope mcp:tools,之後在 Resource Server 用它來限制對工具的存取。
這很重要,原因有二:
- 沒有 openid,你拿不到 id_token 與部分標準 OIDC 欄位。
- 沒有自訂 scope,就無法在 MCP 伺服器端明確表示:「這個權杖可用來呼叫我的工具」。
5. 權杖設定:存活時間、簽章與 claims
接著看看 Keycloak 會簽發哪些權杖,以及如何為 MCP 情境調整。
Access token 的存活時間
Keycloak 在 realm 設定中有 Tokens 區塊,你可以設定:
- Access Token Lifespan;
- Refresh Token Lifespan 與其他逾時值。
對 ChatGPT App 來說,短效的 access tokens 很重要:
- 幾分鐘到幾小時都是合理;
- 若權杖過期,MCP 伺服器回傳 401,ChatGPT 會再次啟動 OAuth 流程,必要時請使用者重新登入。
這與 OpenAI 的 Apps SDK 文件理念一致:短 TTL + 權杖更新,並且能在 IdP 端撤銷權杖時較快地「登出」使用者。
至於 ChatGPT 客戶端的 refresh tokens,一般不是那麼關鍵,或是給予較短期限,避免長期會話。
我們希望在權杖中看到哪些 claims
最小需求:
- sub——使用者在 Keycloak 中的唯一識別;
- iss——權杖簽發者(issuer);
- aud——權杖的目標資源(供 MCP 伺服器使用);
- exp——到期時間;
- scope——scopes 清單。
另外常見的有用欄位:
- email——若你想看到使用者的地址;
- tenantId 或類似 claim——用於多租戶情境;
- roles——做更細緻的授權。
在 Keycloak 中可透過 Protocol Mappers 設定:
- 對 email、preferred_username 等標準 mapper;
- 對使用者屬性的自訂 mapper(user.attribute → claim.name)。
範例:一個將 email 加入為權杖 claim 的 mapper,設定 user.attribute=email, claim.name=email。
在 MCP 伺服器端,你可以從解析後的 JWT 中取得這些 claims,並:
- 把 sub 對應到你的 accountId;
- 用 tenantId 僅擷取屬於該租戶的資料;
- 用 roles 做更細粒度的權限控管。
權杖簽章與 JWKS
Keycloak 預設使用非對稱演算法(通常是 RS256)來簽署 access/id tokens, 並透過 OpenID Discovery 文件中的 JWKS endpoint 公開公鑰。
這對我們很重要,因為 MCP 伺服器能:
- 從權杖取出 issuer;
- 透過 /.well-known/openid-configuration 找到 JWKS endpoint;
- 取得公鑰並在本地驗簽權杖。
這部分會在下一堂介紹「MCP 伺服器作為受保護資源」時更深入說明,但現在先理解為何 Keycloak 會提供這些中繼資料。
6. Dynamic Client Registration (DCR):什麼時候需要
這一節偏進階。到目前為止,我們都是在後台「手動」建立 client,這已足夠啟動 App。 但 OAuth 協議也允許客戶端透過專用 endpoint 動態註冊。
在 ChatGPT 與 MCP 的脈絡中,OpenAI 明確指出平台可能會使用 Dynamic Client Registration。 也就是說,ChatGPT 會透過 discovery 文件中的 registration_endpoint,在 Auth Server「動態」註冊自己。
在 Keycloak 上可這樣理解:
- 在 realm 層級開啟 DCR;
- 你設定政策:誰可以註冊新客戶端、允許哪些 grant types/scopes。
註冊一個使用 Authorization Code + PKCE 並帶有 openid mcp:tools scope 的 public client,其 JSON 範例如下:
{
"clientName": "My ChatGPT App",
"redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
"grantTypes": ["authorization_code"],
"responseTypes": ["code"],
"scope": "openid mcp:tools",
"tokenEndpointAuthMethod": "none"
}
其中 tokenEndpointAuthMethod: "none" 表示這是沒有 client_secret 的 public client。
在本課你只需要知道:
- 若客戶端很多或生命週期很短,DCR 很有用;
- ChatGPT 可能會自行到你的 IdP 註冊;
- 但在初期,你可以只用 UI 建立的靜態 client 即可。
7. 這與我們的教學應用有什麼關係
回想我們的教學用 MCP 伺服器(例如 GiftGenius),它能:
- 提供可能的禮物清單;
- 保存使用者的心願清單;
- 之後——串接商務面、建立訂單等。
當 MCP 伺服器是開放的,它並不知道誰在呼叫它:
- 來自 ChatGPT 的請求邏輯上可能是「Alice」或「Bob」,但 MCP 伺服器無法分辨;
- 你無法顯示私人禮物紀錄;
- 你無法確認要從哪個帳號扣款。
將 Keycloak 設為 Auth Server 之後,情況改變:
- ChatGPT 從你的 MCP 資源的 .well-known 理解該資源受保護並需要權杖。
- ChatGPT 依 Authorization Code + PKCE 流程把使用者導向至 Keycloak。
- 使用者登入(例如我們的 alice)。
- ChatGPT 取得 access token,其中包含 sub、email、mcp:tools 等 claims。
- ChatGPT 呼叫 GiftGenius 工具,並附上 Authorization: Bearer <token>。
- MCP 伺服器在驗證權杖後就能知道:「啊,這是 Alice,sub=... 且 tenantId=demo-tenant」,並據此回應。
這個串接會在下一堂課完成,我們會把 MCP 伺服器變成真正的 resource server:實作中繼資料 endpoint、權杖驗證,以及與使用者的綁定。
8. 小型實作示例(我們的技術棧:TypeScript + Node)
以下不是「唯一正確」的做法,而是典型 Node/TypeScript 技術棧的參考實作。 如果你目前更專注於在 Keycloak 裡點選設定,本節可先快速瀏覽,等要串 MCP 伺服器時再回來。
雖然 Keycloak 的設定大多透過 UI 或其 Admin REST API 完成,但展示一些周邊程式碼有助於理解 你會如何在 MCP 伺服器端使用這些設定。
假設我們已有一個基於官方 SDK 的 Node.js MCP 伺服器(TypeScript)。
授權設定(issuer 與 audience)
建立一個小模組 authConfig.ts:
// authConfig.ts
export const authConfig = {
issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
audience: 'https://mcp.my-company.com', // 你的 MCP 伺服器的 URL
requiredScopes: ['mcp:tools'], // 在權杖中至少需要具備
};
這裡的 issuer 是 Keycloak realm 的 URL;audience 是資源的識別(我們稍後會在權杖與 MCP 設定中使用它)。
透過 JWKS 做基本的 JWT 驗證
在實務中,你八成會使用像 jsonwebtoken + jwks-rsa 或 MCP SDK 的現成工具。最簡骨架如下:
// verifyToken.ts
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { authConfig } from './authConfig';
const client = jwksClient({
jwksUri: `${authConfig.issuer}/protocol/openid-connect/certs`,
});
function getKey(header: any, callback: any) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.getPublicKey();
callback(err, signingKey);
});
}
export function verifyAccessToken(token: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: authConfig.audience,
issuer: authConfig.issuer,
},
(err, decoded) => (err ? reject(err) : resolve(decoded)),
);
});
}
當然,錯誤處理與金鑰快取應更謹慎,不過你已看出重點: Keycloak 會公開 JWKS 金鑰,我們拉取後即可驗簽。
檢查 scope 並擷取身分
在 MCP tools 的 middleware,你可以這樣做:
// authMiddleware.ts
import { verifyAccessToken } from './verifyToken';
import { authConfig } from './authConfig';
export async function requireAuth(bearerToken: string) {
const token = bearerToken.replace(/^Bearer\s+/i, '');
const decoded: any = await verifyAccessToken(token);
const scopes = (decoded.scope as string).split(' ');
const hasScope = authConfig.requiredScopes.every(s => scopes.includes(s));
if (!hasScope) {
throw new Error('Insufficient scope');
}
return {
userId: decoded.sub,
email: decoded.email,
tenantId: decoded.tenantId,
};
}
接著在 MCP 工具的處理器中,你會利用 userId 與 tenantId 載入該使用者的禮物清單。 工具本身我們已在前面模組實作過,現在要緊的是看懂 Keycloak 的權杖如何在你的後端轉化為可用的身分資訊。
9. 設定 Keycloak 為 MCP Auth Server 時的常見錯誤
錯誤 1:使用帶有 client_secret 的 confidential client。
有時出於習慣,會建立 confidential 類型的 client,然後嘗試把 client_secret 寫進 MCP/ChatGPT 的設定。 在 ChatGPT App 生態中,這不應該(也不會)安全運作:ChatGPT 是 public client,它無法保存 secret。 正確作法是 public client + PKCE。
錯誤 2:預設 scopes 過於寬鬆。
把 profile、email 等一堆標準 scopes 都開著,然後發給每個聊天使用,並不是好主意。 最好最小化:openid 與明確的 mcp:tools(或少數應用相關 scopes)就足夠做第一版。 這能降低資料外洩風險,並讓行為更可預期。
錯誤 3:錯誤的 redirect URI。
經典案例:Keycloak 設的是 http://localhost:5173/callback,但 MCP Jam 卻走 http://localhost:5173/,或反之。 結果就是 invalid_redirect_uri,讓除錯非常挫折。 請務必核對 Jam/ChatGPT 文件中的 redirect URI,逐字填入。
錯誤 4:未啟用 PKCE 或方法不正確。
有些版本的 Keycloak 需要你另外勾選「PKCE required」,並指定方法為 S256。 若沒設定,期待 PKCE 的 ChatGPT/Jam 可能會收到 invalid_request,並抱怨 code_challenge。 請一定檢查 public client 的 PKCE 設定。
錯誤 5:權杖中的 claims 錯誤或缺失。
有時會因為沒有啟用對應 scope 或沒有設定 protocol mapper,導致權杖裡沒有 sub 或 email。 結果是在 MCP 伺服器端雖然看到權杖,卻無法把它映射到真實使用者。 解法:確定必要欄位(至少 sub,最好還有 email/tenantId)有被加入 access/id tokens。
錯誤 6:access tokens 的 TTL 過長。
從安全角度,把 access tokens 簽到一天/一週很糟。 一旦權杖外洩,攻擊者就能長期存取 MCP 資源。 改用短效的 access tokens(數分鐘或數小時),必要時再重新授權。
錯誤 7:把東西都塞進 master realm,造成 realm 混亂。
常見做法是直接在 master realm 建 client 與使用者。 後來又加了幾個專案——最後誰跟誰都搞不清楚。 較好的做法是一開始就為每個應用/課程建立獨立 realm。這會讓你與 DevOps 都輕鬆許多。
GO TO FULL VERSION