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:谁向谁发令牌、用户在哪里登录、以及为什么 MCP 服务器永远看不到用户的密码;
  • 把它同我们的 Next.js/MCP 后端以及未来的 Keycloak/Auth0 配置关联起来。

今天我们做的事情:

  • 不在 Keycloak 里点勾,也不配置具体的 IdP;
  • 不写完整的 JWT 校验或 introspection——这些留到后续讲(Auth Server 与作为受保护资源的 MCP Server)。

当前目标——让你能拿起一张纸,在 ChatGPT、你的服务器与 Auth0/Keycloak 之间画出箭头,并且不带停顿地解释:哪里登录、哪里发令牌、哪里拿数据。

2. 信任三角:MCP Client、MCP Server、MCP Auth Server

先看角色。技术上的“信任三角”由 MCP ClientMCP ServerMCP Auth Server 构成;用户(User)是独立的角色、资源的所有者,站在这个三角之外并对访问授予同意。在 MCP 与 Apps SDK 的语境中,这个架构被明确地形式化了。

User(资源所有者)

这是屏幕另一端的人。他会:

  • 登录 ChatGPT;
  • 发出请求“展示我的订单/我的礼物清单”;
  • 同意把你服务的账号“绑定”到 ChatGPT。

关键点:他拥有资源(订单历史、个人资料、礼物清单),并由他对访问授予同意。

MCP Client

对我们而言它是:

  • 带 Apps SDK 的 ChatGPT;
  • 有时是 MCP Jam Inspector(调试时)。

MCP Client 能:

  • 读取你的 MCP 服务器的元数据(通过 .well-known);
  • 在用户浏览器里启动 OAuth 流程;
  • 保存并把令牌附在对 MCP 工具的调用上。

要记住,MCP Client 是公共客户端(public client)。它不保存你的 client_secret,因此它与 Auth Server 的交互就像一个公共 SPA 应用:Authorization Code + PKCE。

MCP Server(资源服务器)

这是你的实现 MCP 的后端:

  • 与 ChatGPT 建立连接;
  • 声明工具(tools)、资源、prompts;
  • 对每次工具调用检查 Authorization: Bearer <token> 头;
  • 验证令牌(签名、expaudscope),若一切正常,执行业务逻辑。

根本原则:MCP 服务器不负责登录。它看不到密码,不渲染登录表单,不向用户发“请确认邮箱”的邮件。它只信任来自 Auth Server、经加密签名的令牌。

MCP Auth Server(授权服务器 / IdP)

这是一个独立的认证与授权服务:Keycloak、Auth0、Ory Hydra+Kratos、Okta、Cognito、Azure AD 等。

它负责:

  • 登录 UI(邮箱/密码、SSO、2FA);
  • 存储用户账号;
  • 签发令牌(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,保存令牌,调用 MCP 工具 不校验密码,不验证令牌签名
MCP Server 验证令牌,执行工具的业务逻辑 不渲染登录表单,不保存密码
MCP Auth Server 让用户登录,签发令牌 不关心你的 MCP 工具及其业务逻辑

如果你脑海里一直把这些混成一个“什么都做的大服务器”——现在是时候拆分了。

3. Flow 长什么样:从“没有令牌”到对工具的受保护调用

现在看看消息流。在 MCP 规范里,这个过程被称为“The Flow”:discovery → redirect → code → token → authorized calls。

第 0 步:在没有令牌时尝试调用受保护的工具

用户说:“把我保存的礼物创意给我看看。”

作为 MCP Client 的 ChatGPT 判断:“这需要调用我们 MCP 服务器的 getUserGiftLists 工具。”它在没有令牌的情况下发起调用(毕竟用户还没登录)。

你的 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 流程并绑定账号。”

Discovery:.well-known/oauth-protected-resource

MCP Client 接着向你的服务器请求元数据:

GET /.well-known/oauth-protected-resource

服务器返回一个 JSON 文档,其中包含资源标识,以及需要从哪些授权服务器获取令牌。

一个最小示例(细节稍后配置,这里先理解思路):

{
  "resource": "https://api.giftgenius.com",
  "authorization_servers": [
    "https://auth.giftgenius.com"
  ],
  "scopes_supported": ["gifts.read", "gifts.write"]
}

这里:

  • resource——你的资源的规范 ID;之后签发令牌时应把它作为 audienceresource
  • authorization_servers——ChatGPT 可以去请求令牌的 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 可以读取 TA 的礼物清单(scope gifts.read)。

Code → Token:用 PKCE 将授权码换成令牌

登录成功后,Auth Server 把用户重定向回 MCP Client,并带上 code。MCP Client:

  • /token 发起 POST;
  • 携带 codecode_verifier(它对应上一步的 code_challenge)。

Auth Server 校验 PKCE:对 code_verifier 做哈希,与原始的 code_challenge 比较。若一致且客户端确为发起该 flow 的同一方,则:

  • 签发短期有效的 access_token(通常是 JWT);
  • 在其中写入:
    • sub——该用户在 Auth Server 的 ID;
    • audresource——你的 MCP 服务器;
    • scope——被允许的动作(gifts.readopenid 等)。

Authenticated Request:带令牌调用 MCP 工具

现在 MCP Client 可以再次调用你的工具,但这次带上头:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

MCP 服务器:

  • 验证令牌签名(用 Auth Server 的 JWK)或通过 introspection;
  • 检查有效期(exp);
  • 检查 aud/resource——令牌确实是为 https://api.giftgenius.com 签发的;
  • 查看 scope,判断是否允许调用 getUserGiftLists

随后它便可基于某个 userId 查询你的数据库并返回该用户的礼物清单。

注意,到目前为止我们只谈了网络层面的流程:令牌如何获取并到达 MCP 服务器。接下来要理解,如何从令牌中的 sub 等 claims 得到你数据库里的具体 userId——这时就轮到 identity bridge 登场了。

4. Identity Bridge:ChatGPT 的用户如何变成你数据库中的 userId

架构中最有意思的一段是“身份桥”(identity bridge)。MCP 规范明确强调:MCP 服务器并不认识 ChatGPT 的用户,它依赖的是 Auth Server 发来的令牌中的数据。

大致示意如下:

flowchart TD
  User[用户(在 ChatGPT)] -->|Login/SSO| Auth[Auth Server]
  Auth -->|JWT: sub, email, tenant| MCP[MCP Server]
  MCP -->|userId/tenantId| DB[(你的数据库)]

分步来看如下。

首先,Auth Server 内部管理自己的用户:有 useremailid,也可能有 tenantroles。登录成功后,它把这些信息放入令牌(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 在验证令牌后,取出这些 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 服务器同时展示登录、自己签发令牌吧。”形式上可行(你可以在其中嵌一个迷你 IdP),但从架构上看这不是好主意。原因非常现实。

首先是安全与可扩展性。Auth Server 是台“重型机器”:2FA、社交登录、密码策略、账号锁定、找回、登录审计,甚至合规认证。让每个微服务(每个 MCP 服务器)都重造一遍——是直通地狱与 PCI‑DSS 的高速路。把这些交给 Keycloak/Auth0,你只需要验证它们的令牌简单得多。

其次是客户端的可替换性。今天你只有 ChatGPT。明天你会接入 Claude Desktop、自家基于 Next.js 的 Web 前端、移动应用。它们都可以共用同一个 Auth Server 与同一套 OAuth 2.1 流程,而你的 MCP 服务器只需继续验证令牌。完全不必为每个新客户端重写业务逻辑。

第三是代码整洁。理想状态下,MCP Server:

  • 能发布 /.well-known/oauth-protected-resource
  • 能验证 Bearer 令牌并从中取出 userIdscopestenant
  • 实现业务工具(orders、gifts、profiles)。

所有登录 UI——表单、样式、社交登录——都驻留在 Auth Server,不会把后端搞得臃肿。

6. 在我们的教学应用 GiftGenius 中长什么样

回到我们贯穿课程的应用。假设我们有:

  • ChatGPT App “GiftGenius” 带一个小组件(Apps SDK),能帮忙挑礼物;
  • 基于 Node/Next.js 的 MCP 服务器,提供以下工具:
    • searchGifts——匿名工具,不需要登录;
    • getSavedGiftLists——个人工具,需要认证;
  • Auth Server(稍后使用 Keycloak/Auth0),每个用户都有账号。

匿名用户与已登录用户的场景

当用户只说“给我弟挑个礼物,30 岁,喜欢桌游”,我们的 App 可以:

  • 调用匿名工具 searchGifts
  • 在界面中给出推荐。

在这种情况下:

  • 不需要令牌;
  • MCP 服务器直接执行请求(比如查询你的目录或第三方 API)。

一旦用户说“把这个保存到我的清单”或“展示我保存的创意”,模型就会调用受保护的 getSavedGiftLists。服务器返回 401 + 带 resource_metadataWWW-Authenticate。ChatGPT 启动 OAuth 向导“Link GiftGenius account”,引导用户登录并获取令牌。

之后每次受保护的调用:

  • MCP Server 都能看到 Authorization: Bearer ...;
  • 从令牌中取出 userId
  • 据此 userId 过滤数据。

正因为如此,我们可以:

  • 隔离不同用户的数据;
  • 安全地展示订单历史、收藏清单;
  • 实现交易功能(课程后续)。

后端架构:middleware + 工具处理器

在 Node/Next.js 代码里,这通常是一条链:“认证中间件 → 工具的业务处理器”。在讲工具处理器实现时我们强调过,要把上下文传进去:user_id、令牌、配置等。

代码片段可以是这样:

// auth-context.ts
export type AuthContext = {
  userId: string | null;    // 对匿名调用为 null
  scopes: string[];
};

挂在所有 MCP 端点上的中间件:

// 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); // 令牌校验
  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 }
  });
}

要点在于,工具处理器对 OAuth、PKCE 一无所知。它只处理一个“显而易见”的 userId。所有 OAuth 的魔法都在它之前完成:在 MCP 客户端与 Auth 中间件里。

7. 可视化:Client、Server 与 Auth 如何协作

我们已经在第 3 节用文字走完了流程。有时画图更直观,所以这里用两张图展示相同的交互。

交互骨架(The Triangle of Trust)

flowchart TD
  U[用户] -->|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 实质上确认其身份并签发令牌。MCP Client 驱动这一过程,随后用令牌访问 MCP 服务器。MCP 服务器看不到登录名和密码,只看到令牌,并据此决定允许什么。

从请求到响应的顺序流

sequenceDiagram
  participant User
  participant ChatGPT as MCP Client
  participant Auth as Auth Server
  participant MCP as MCP Server

  User->>ChatGPT: "展示我的礼物清单"
  ChatGPT->>MCP: callTool(getSavedGiftLists)(无令牌)
  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 签发令牌。每个资源服务器都必须检查令牌是否确实面向它,否则就会出现经典的“confused deputy”问题:为某个服务签发的令牌被另一个服务接受。

其次,你可以有很多客户端:

  • ChatGPT App;
  • 你自己的前端;
  • 移动应用;
  • 通过 MCP Gateway 的合作方集成。

它们都会:

  • 读取 /.well-known/oauth-protected-resource
  • 获知 Auth Server 的位置;
  • 走 OAuth 2.1 流程;
  • 取得令牌并调用 MCP 服务器。

第三,现代 Auth Server 越来越多地支持动态客户端注册(DCR)——通过 API 动态注册客户端。MCP 规范正是预设了这种能力:客户端(ChatGPT/Jam)可以根据 registration_endpoint 在 Auth Server 上自动为自己注册。

在本模块里需要理解:

  • MCP Client、MCP Server 与 Auth Server 通过标准化的 discovery 文档与令牌交互;
  • 你不需要在后端代码里“硬编码”所有客户端;
  • 你可以扩建生态,而不破坏既有的授权模型。

9. MCP 授权架构中的常见理解误区

错误 1:“MCP 服务器应当自己让用户登录”。
有时开发者想把登录表单直接塞进 MCP 服务器,然后通过工具传递登录名/密码。这破坏了 OAuth 的理念。MCP 服务器在任何情况下都不该看到密码。登录与同意属于 Auth Server 的职责。MCP 服务器只处理令牌及其 claims。

错误 2:混淆 MCP Client 与 MCP Server。
有人把 ChatGPT 视作“我后端的一部分”,于是尝试在其中保存机密,或指望它自己做权限验证。实际上 MCP Client 只是发起 OAuth 并附上令牌。验证令牌与权限是 MCP 服务器的职责,而不是 ChatGPT 的。

错误 3:用 .env 里的 API Key 代替 OAuth。
典型反模式:搞一个大的 SERVICE_API_KEY,放在 MCP 服务器的 .env 里就觉得万事大吉。这样没有按用户粒度的权限划分,无法安全展示个人数据或执行购买,一切都以“服务”的身份做事,而非用户。这与 ChatGPT Apps 的授权目标完全相悖。

错误 4:忽略 audienceresource
如果 MCP 服务器接受任何签名正确的 JWT,而不看 aud/resource,那么同一 Auth Server 为别的服务签发的令牌也可能被用来调用你的工具。这直接违反了 OAuth 的安全模型。服务器必须验证令牌确实是为它的 resource 签发的。

错误 5:把认证逻辑和业务逻辑搅在一起。
有时会把解析令牌、签名验证、JWK 处理等都塞进工具处理器,导致代码脆弱难维护。更好的方式是把“验证令牌、映射到 userId”这一层(middleware)与“具体工具逻辑”分离开来,让后者接收一个清晰的 AuthContext

错误 6:指望 ChatGPT 在没有 .well-known 的情况下“自己搞定”。
如果没有正确的 /.well-known/oauth-protected-resource 端点,MCP 客户端根本不知道你的 Auth Server 在哪,也不知道需要哪些 scopes。结果就是聊天界面“不会登录”,而开发者盯着空日志发呆。正确做法:MCP 服务器通过 .well-known 清晰声明自己的授权要求,客户端读取后构建流程。

错误 7:在业务逻辑里把用户给忘了。
有时即使正确配置了 OAuth 并把令牌映射到了 userId,开发者在数据库查询里却没用起来:比如忘了用 ownerId = userId 做过滤。这样任何已授权用户都可能看到别人的数据。拥有令牌只是第一步;第二步永远是在业务代码里正确使用 userIdscope

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