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 适合课程与真实生产:
- 它是开源的,易于本地或 Docker 启动;
- 实体模型清晰:realm、clients、users、roles;
- 基本上,你在 Keycloak 上学到的配置,迁移到 Auth0/Okta/Cognito 时几乎可以 1:1 套用:核心概念相同——client、scopes、redirect URIs、PKCE。
重要理念:我们配置的不是“整个平台都用的 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-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 就是“活生生”的用户:具有用户名、密码、邮箱、属性、群组与角色。 当有人通过 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:它可以安全保存密钥。
在 ChatGPT 的世界里则相反:
- OAuth 客户端是 ChatGPT 平台自身或 MCP Jam 之类的工具;
- 你无法控制它的代码与运行环境;
- 任何你交给 ChatGPT 的 client_secret 都应视为立刻已泄露。
因此 ChatGPT/Jam 作为public clients 工作,即不使用 client_secret, 并用 PKCE(Proof Key for Code Exchange)来补偿。
通俗理解 PKCE 是做什么的
PKCE 就是“每个会话一次性使用的秘密”。它的目标是防止别人仅凭拦截到的 authorization code,就能在别处换出令牌。 基本流程如下:
- 客户端生成一个随机字符串 code_verifier。
- 对其进行哈希(通常为 SHA-256),得到 code_challenge。
- 重定向到 /authorize 时,发送 code_challenge 和 code_challenge_method=S256。
- 登录后,用户携带 code 返回到 redirect URI。
- 客户端向 POST /token 发起请求,提交 code 与原始的 code_verifier。
- 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 服务器端,你可以解码令牌,取出 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(在 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 错误。
本课程中有两个典型客户端:
- MCP Jam/Inspector 用于调试。它们通常运行在 http://localhost:PORT/...。本地场景下,合理的允许列表可以是:
- http://localhost:5173/*,或 Jam 实际使用的具体路径。
- ChatGPT / Apps SDK 的生产环境。此处的 redirect URI 由平台自行定义。 在真实集成中,你需要参考 OpenAI 的最新文档,写入 ChatGPT 用作回调的 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 的生命周期
在 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 配置:
- 对 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 令牌进行签名, 并通过 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 之后,情况会发生变化:
- 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 的 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 工具的处理器中,你会使用 userId 与 tenantId 来加载该用户的礼物清单等数据。 这些工具我们在之前的模块已实现,这里要看到的是: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 过于宽泛。
保留 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 不正确或缺失。
有时令牌里没有 sub 或 email,原因是相应的 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 都更省心。
GO TO FULL VERSION