CodeGym /课程 /ChatGPT Apps /配置 MCP 认证服务器:以 Keycloak 为例

配置 MCP 认证服务器:以 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 适合课程与真实生产:

  • 它是开源的,易于本地或 Docker 启动;
  • 实体模型清晰:realmclientsusersroles
  • 基本上,你在 Keycloak 上学到的配置,迁移到 Auth0/Okta/Cognito 时几乎可以 1:1 套用:核心概念相同——clientscopesredirect URIsPKCE

重要理念:我们配置的不是“整个平台都用的 Keycloak”,而是为我们的 ChatGPT App 专门创建一个 Realm。它相当于专为 MCP 客户端认证准备的“沙盒”。

总之,学完本讲你将拥有:

  • 一个为 ChatGPT 应用准备的 Keycloak realm;
  • 启用 Authorization Code + PKCE 的 public client;
  • 一组最小但足够的 scopes 与 claims;
  • 理解令牌在你的 Node‑MCP 服务器中的生命周期。

2. 以 MCP 视角理解 Keycloak 的基础实体

为了不在管理界面里迷路,我们先把实体捋清楚。

Realm:配置与用户的空间

Keycloak 的 Realm 是一个隔离的空间,拥有自己的用户、客户端与策略。 一个易懂的比喻是“写字楼里租用的独立办公室”:各自有独立的房间、员工名单和入场规则。

对课程和你的第一个真实应用来说,建议创建独立的 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 就是“活生生”的用户:具有用户名、密码、邮箱、属性、群组与角色。 当有人通过 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:它可以安全保存密钥。

在 ChatGPT 的世界里则相反:

  • OAuth 客户端是 ChatGPT 平台自身或 MCP Jam 之类的工具;
  • 你无法控制它的代码与运行环境;
  • 任何你交给 ChatGPT 的 client_secret 都应视为立刻已泄露。

因此 ChatGPT/Jam 作为public clients 工作,即不使用 client_secret, 并用 PKCE(Proof Key for Code Exchange)来补偿。

通俗理解 PKCE 是做什么的

PKCE 就是“每个会话一次性使用的秘密”。它的目标是防止别人仅凭拦截到的 authorization code,就能在别处换出令牌。 基本流程如下:

  1. 客户端生成一个随机字符串 code_verifier
  2. 对其进行哈希(通常为 SHA-256),得到 code_challenge
  3. 重定向到 /authorize 时,发送 code_challengecode_challenge_method=S256
  4. 登录后,用户携带 code 返回到 redirect URI。
  5. 客户端向 POST /token 发起请求,提交 code 与原始的 code_verifier
  6. Keycloak 对 verifier 做同样的哈希,与 challenge 比对,如果一致则签发令牌。

对我们而言的重点:以上步骤均由 ChatGPT/MCP Jam 代为完成。 我们只需要在 Keycloak 的客户端配置里启用 Authorization Code + PKCE(S256),并且不要求 client_secret

4. 针对 MCP 场景逐步配置 Keycloak

既然已确定:对于 ChatGPT/MCP Jam,我们需要一个public client, 启用 Authorization Code Flow 与 PKCE(S256),且不使用 client_secret。 下面看看这在 Keycloak 设置中应如何体现。

假设你已拥有可运行的 Keycloak(Docker 容器、本地安装——都可以)。 此处我们关注设置的思路,而不是点击路径。

为应用创建新的 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(在 UI 上通常叫 “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/...。本地场景下,合理的允许列表可以是:
    • http://localhost:5173/*,或 Jam 实际使用的具体路径。
  2. ChatGPT / Apps SDK 的生产环境。此处的 redirect URI 由平台自行定义。 在真实集成中,你需要参考 OpenAI 的最新文档,写入 ChatGPT 用作回调的 URL。

在本讲中要理解的关键是:ChatGPT 不能随便使用任意 redirect,它必须与 Auth Server 中登记的值完全一致。因此:

  • 切勿使用 * 之类的“任意 URL 都行”;
  • 本地开发中在 localhost 范围内可使用通配符,但生产环境不要这样做。

Scopes:最小,但足够

Scopes 是客户端请求的一小组权限列表。

对于我们的 MCP 场景,通常需要:

  • openid——启用 OpenID Connect,并获得带有 sub、有时还有 emailid_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 的生命周期

在 realm 设置中,Keycloak 有 Tokens 版块,你可以配置:

  • Access Token Lifespan;
  • Refresh Token Lifespan 以及其他超时。

对 ChatGPT 应用来说,短生命周期的 access token 更合适:

  • 几分钟到几小时都可接受;
  • 如果令牌过期,MCP 服务器返回 401,ChatGPT 会重新走 OAuth 流,必要时用户再登录一次。

这与 OpenAI 的 Apps SDK 文档一致:短 TTL + 刷新/重新授权,且一旦需要,可在 IdP 侧迅速吊销令牌,实现“快速登出”。

对 ChatGPT 客户端而言,refresh token 要么不关键,要么也应设置较短的有效期,避免长期会话。

我们希望令牌中包含哪些 claims

最少应该有:

  • sub——用户在 Keycloak 中的唯一标识;
  • iss——签发者(issuer);
  • aud——令牌的目标资源(稍后在 MCP 服务器使用);
  • exp——过期时间;
  • scope——scope 列表。

此外常用的还有:

  • 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 令牌进行签名, 并通过 OpenID Discovery 文档中的 JWKS endpoint 发布公钥。

这对我们很关键,因为 MCP 服务器可以:

  • 从令牌中读取 issuer
  • 通过 /.well-known/openid-configuration 查到 JWKS endpoint;
  • 获取公钥并在本地验证令牌签名。

这部分我们会在关于“把 MCP 服务器做成受保护的资源服务器”的讲次中展开,但现在先知道为什么 Keycloak 会提供这些元数据。

6. 动态客户端注册(DCR):什么时候需要

这是一个进阶主题。到目前为止我们都是在管理界面“手工”创建客户端,这已经足以启动应用。 但 OAuth 协议允许客户端通过专门的 endpoint 动态注册自己。

在 ChatGPT 与 MCP 的上下文中,OpenAI 明确提到平台可能使用 Dynamic Client Registration。 也就是说,ChatGPT 可以通过 discovery 文档中的 registration_endpoint 在你的 Auth Server 上“即时”注册自己。

在 Keycloak 层面,这通常为:

  • 在 realm 级别开启 DCR;
  • 配置策略:谁可以注册新客户端,以及可用的 grant types/scopes。

一个注册 public client(Authorization Code + PKCE、scope 为 openid mcp:tools)的示例 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 创建一个静态客户端就足够了。

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 的 UI,本节可先粗读,等接入 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'],          // 最少需要出现在令牌中的 scope
};

这里的 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 工具的中间件中,你可以这样做:

// 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 类型的客户端,并试图在 MCP/ChatGPT 配置中填写 client_secret。 在 ChatGPT App 生态中,这既不应工作也不安全:ChatGPT 是 public client,无法保存密钥。 正确做法是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 不正确或缺失。
有时令牌里没有 subemail,原因是相应的 scope 或 protocol mapper 未配置好。 结果是在 MCP 服务器端看到了令牌,却无法映射到真实用户。 解决办法:确认必要字段(至少 sub,最好还有 email/tenantId)被映射进 access/id 令牌。

错误 6:access token 的 TTL 过长。
从安全角度看,发放有效期为一天/一周的 access token 不是好主意。 一旦令牌泄露,攻击者就能长期访问 MCP 资源。 更好的做法是让 access token 短期有效(分钟或小时),需要时再触发重新授权。

错误 7:混用 realm 或直接使用 master。
有时第一步就是在 master realm 里创建客户端与用户, 随后又在其中接了好几个项目——最终一团乱。 最好一开始就为每个应用/课程单独建立 realm。这样对你和 DevOps 都更省心。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION