CodeGym /課程 /ChatGPT Apps /設定 MCP Auth Server:以 Keycloak 為例

設定 MCP Auth Server:以 Keycloak 為例

ChatGPT Apps
等級 10 , 課堂 2
開放

1. 實務中的 Auth Server 是什麼,為什麼選 Keycloak

先快速複習一下:Auth Server(IdP)是一個服務,它:

  • 向使用者顯示登入/註冊與同意(consent)畫面;
  • 發出 OAuth/OIDC 權杖(access_tokenid_tokenrefresh_token);
  • 發布 discovery 文件與 JWKS 金鑰,讓資源伺服器可以驗證這些權杖。

在我們的堆疊中:

  • ChatGPT / MCP Jam 充當 OAuth 客戶端(public client);
  • 你的 MCP 伺服器 作為 Resource Server
  • Keycloak 作為 Auth Server

為什麼 Keycloak 對課程與實務都很方便:

  • 它是 open source,容易在本機或 Docker 起來;
  • 它的實體模型相當清楚:realmclientsusersroles
  • 基本上,你在 Keycloak 學到的設定,之後幾乎可以 1:1 移植到 Auth0/Okta/Cognito:觀念相同——clientscopesredirect URIsPKCE

關鍵觀念:我們設定的不是「整個專案共用的 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-mcpmcp-course。這樣可以:

  • 不去動 master realm,避免不小心弄壞管理介面;
  • 透過匯出/匯入 realm,在不同環境(dev / staging / prod)之間重用設定與使用者。

Client:應用程式的登記(ChatGPT / MCP Jam)

在 Keycloak 中,Client 並非「使用者」,它是向 Auth Server 取用權杖的應用程式。 在我們的案例中,這不是你的 Next.js 後端,而是 MCP 客戶端: ChatGPTMCP 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.combob@example.com);
  • 也可以加幾個屬性,例如 tenantplan,以示範權杖中的 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 就是一個「每個工作階段的一次性祕密」。它的目標是避免授權碼被攔截後,能在別處兌換成權杖。 流程如下:

  1. 客戶端產生一個隨機字串 code_verifier
  2. 對它做雜湊(通常是 SHA-256)得到 code_challenge
  3. 在導向至 /authorize 時一併送出 code_challengecode_challenge_method=S256
  4. 完成登入後,使用者會帶著 code 回到 redirect URI。
  5. 客戶端對 /token 發出 POST,同時傳遞 code 與原始的 code_verifier
  6. 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 伺服器中,你能解碼權杖,取出 subemailtenantId,並把它們連結到你的使用者模型。

為 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 錯誤。

在本課程中有兩種典型客戶端:

  1. MCP Jam/Inspector 用於除錯。它們通常在 http://localhost:PORT/... 運作。對本機情境,合理允許如下 redirect:
    • http://localhost:5173/* 或 Jam 使用的具體路徑。
  2. 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,例如 profileemail
  • 新增 scope mcp:tools,之後在 Resource Server 用它來限制對工具的存取。

這很重要,原因有二:

  1. 沒有 openid,你拿不到 id_token 與部分標準 OIDC 欄位。
  2. 沒有自訂 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 設定:

  • emailpreferred_username 等標準 mapper;
  • 對使用者屬性的自訂 mapper(user.attributeclaim.name)。

範例:一個將 email 加入為權杖 claim 的 mapper,設定 user.attribute=emailclaim.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 之後,情況改變:

  1. ChatGPT 從你的 MCP 資源的 .well-known 理解該資源受保護並需要權杖。
  2. ChatGPT 依 Authorization Code + PKCE 流程把使用者導向至 Keycloak。
  3. 使用者登入(例如我們的 alice)。
  4. ChatGPT 取得 access token,其中包含 subemailmcp:tools 等 claims。
  5. ChatGPT 呼叫 GiftGenius 工具,並附上 Authorization: Bearer <token>
  6. 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 工具的處理器中,你會利用 userIdtenantId 載入該使用者的禮物清單。 工具本身我們已在前面模組實作過,現在要緊的是看懂 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 過於寬鬆。
profileemail 等一堆標準 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,導致權杖裡沒有 subemail。 結果是在 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 都輕鬆許多。

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