MCP Server 如何代表用户调用你的 API:生产级 token 策略实践
讲解 MCP 服务器的出站令牌策略:为什么令牌透传和 M2M 方式不可行,以及令牌交换如何让权限与用户保持一致。
讲解 MCP 服务器的出站令牌策略:为什么令牌透传和 M2M 方式不可行,以及令牌交换如何让权限与用户保持一致。
在上一篇文章里,我们分享了 Logto 落地 Remote MCP Server 的整体经验, 这篇文章我们将讲解 Remote MCP Server 架构设计和 OAuth 认证流程的实现细节。
以 MCP Server 为分界,Remote MCP Server 的认证分**入站(inbound)和出站(outbound)**两段:
入站这部分,MCP 规范定义得很清楚,社区的讨论和实践也很充分,我们之前写过一篇实现指南。出站的讨论就少多了:MCP Server 手里只有一张「用于访问 MCP Server」的 token,它要如何代表用户去调用业务 API?
这正是我们在落地 Logto MCP Server 时反复纠结的问题。Logto Cloud 是典型的 B2B 多租户产品:一个用户可以属于多个 tenant,权限由用户在每个 tenant 里的角色决定,AI 不仅要代表用户行事,还要落到正确的 tenant。
这篇文章会沿着我们实际的决策过程展开:
在 OAuth 的视角里,Remote MCP Server 同时扮演两个角色:
也就是说,MCP Server 在两侧扮演不同的角色,各自对应的是不同的 token:MCP client 通过 OAuth 拿到的 token,audience 是 MCP Server,业务 API 校验 audience 时会直接拒绝它。
所以出站的本质是一个身份委托(delegation)问题:MCP Server 如何在不持有用户凭证的前提下,以用户的身份、在用户的权限范围内调用下游 API。
把 MCP endpoint 做进业务 API 服务,共用同一个进程、同一套认证,看起来是最直接的做法。但经过考量,我们还是选择了独立部署:
MCP Server 部署在独立的域名 mcp.logto.io 下,和主服务之间没有任何私有耦合,未来想把它开源出去都没有障碍。
内嵌方案也绕不开 token 问题:MCP endpoint 和业务 API 共享同一个 resource identifier,MCP client 拿到的 token 直接携带全量 API 的权限;「这次调用以谁的权限执行」从跨服务的 token 交换变成进程内的权限传递,问题本身还在。独立部署要求我们把权限边界显式设计出来,这也是这篇文章接下来的内容。
出站的讨论要从一个更基础的问题开始:MCP client 拿到的 token,audience 应该是谁?
直接复用业务 API 的 resource identifier 是最直接的做法:Logto 的 Management API 本身就是标准的 OAuth protected resource,让 MCP client 直接申请它的 token,MCP Server 原样转发。不少 MCP Server 的早期实践就是这么做的。
代价在于:如果 MCP token 的 audience 是业务 API,权限边界就不存在了。
所以我们的第一个决定是:MCP Server 是一个独立的 protected resource,有自己的 resource identifier(https://mcp.logto.io)和对应的 scope。
这个决定让权限边界干净了,出站的问题也随之变得具体:token 只能访问 MCP Server,那 MCP Server 拿什么去调 API?
下面进入方案演进。
第一个想法来自一个很自然的类比。
Logto Console 是一个 SPA,它调用 Management API 的方式很简单:用户在浏览器里 OAuth 登录,拿到 audience 为 Management API 的 token,前端直接带着它调 API。
那 MCP Server 是不是也可以当作另一种 Console?让 MCP client 在登录时直接申请业务 API 的 token,MCP Server 不做任何转换,原样转发:
这个方案的诱人之处就在于它极其简单,MCP Server 只负责转发 token。但是它有显而易见的问题:
第一,它和上一节的权限边界决定直接冲突。 Token 透传要求 MCP client 持有业务 API 的 token,这正是我们刚刚否决掉的事情。
第二,MCP Server 不再是真正的 protected resource。 它收到的 token audience 不是自己,audience 校验形同虚设,只能退化成「验证签名和 issuer」。这不符合 MCP 规范对 authorization 的定义(MCP Server 应作为 resource server,通过 RFC 9728 的 Protected Resource Metadata 声明自己),本质上是把一个后端服务伪装成了浏览器里的 SPA。
第三,MCP client 不配合。 规范的 MCP client 按 Protected Resource Metadata 的指引申请 token,audience 就是 MCP Server。没有标准的方式让它去申请业务 API 的 token,这条路在客户端侧走不通。
既然不能用用户的 token,那用 MCP Server 自己的身份呢?
给 MCP Server 配一个 M2M(machine-to-machine)应用,用 client credentials 拿 token,去调业务 API。这也是很多内部服务之间的标准做法。
入站认证也顺了:MCP client 的 token audience 是 MCP Server,MCP Server 正常校验,然后用自己的 M2M token 干活。
这个方案的致命缺陷是,M2M token 的权限和用户的权限没有任何关系:
M2M 适合定时任务、系统间同步这类没有用户上下文的场景。MCP Server 不一样,每次调用都由具体的用户发起,就应该以这个用户的身份和权限去执行。
前两个方案的教训汇总起来,就是理想方案的要求:
这指向一个标准机制:token exchange(RFC 8693)。MCP Server 拿一张代表用户的凭证去 auth server 交换下游 API 的 token,用户的身份和权限在交换中保留。
在 Logto 里,这张「代表用户的凭证」由 user impersonation 能力提供:subject token。它是服务端为指定用户申请的短期凭证,语义是「接下来这次 token exchange,以这个用户的身份进行」。用户不需要手动创建或配置任何东西,全程自动化;subject token 短期、一次性,用完即失效。
整个架构里有四个角色,MCP Server 独立部署在 mcp.logto.io:
一次 tool 调用背后的完整 token 流转:
逐步拆解:
① 入站验证。MCP client 带着 user token 调用 tool。这个 token 的 audience 是 MCP Server 自己的 resource identifier,scope 是 mcp:all。MCP Server 校验签名、issuer、audience、scope,拿到用户身份。入站到此为止,这个 token 不会再往下游走。
② 服务身份认证。MCP Server 用自己的 M2M 凭证换一个 access token,scope 是专设的 access:mcp:api,它只有一个用途:调用下一步的专用接口。
③ 申请 subject token,整条链路的关键一步。MCP Server 调用 Cloud 专门为 MCP 开的接口 POST /api/mcp/subject-tokens,同时出示两个凭证:
Authorization 头:M2M token,证明「我是官方 MCP Server」x-mcp-user-token 头:用户的 user token,证明「这个用户确实授权过我,且授权仍然有效」Cloud 这边会对 user token 做完整校验:签名、issuer、有效期,audience 必须是 MCP Server 的 resource identifier,scope 必须包含 mcp:all。校验通过后,userId 直接取自 user token 的 sub claim,接口没有「指定用户」的参数。
这样设计是为了防止 M2M 凭证被滥用:如果接口接受任意 userId,拿到 M2M 凭证就能冒充任何用户;在这个设计下,MCP Server 只有在某个用户的有效授权在场时,才能为这个用户兑换凭证。
签发出来的 subject token 短期、一次性,实现里不做缓存,每次使用都现场申请。
④ 换出干活的 token。拿 subject token 走标准 token exchange,按需换两种 token:
⑤ 出站调用。带着换来的 token 调用业务 API,做完事把结果返回给 MCP client。
多租户的 tenant 上下文也在交换这一步解决:tenant 不在连接层,而在交换层。list_tenants 用 Cloud API token 列出选项,用户在对话里选一个,MCP Server 再为选中的 tenant 换 org token。一个 endpoint 服务所有 tenant,不需要为每个 tenant 单独部署,对话中途新建的 tenant 也即刻可用。
把前面每个方案的失败点拿回来对照,会发现它们都被逐一堵上了:
x-mcp-user-token 验证失败,出站链路当场断掉回头看,M2M 在最终方案里有了正确的分工:它负责证明「我是谁」,而「以用户身份行事」的资格,必须由用户的有效授权当场兑换。
回顾整条链路,Remote MCP Server 的 token 策略可以归纳成几条:
一个简单的检验方法:假设 MCP client 手里的 token 泄漏,攻击者能做的应该只有「调用 MCP Server 上那几个受控的 tools,且仍受用户权限约束」。如果能直接调到全量 API,说明权限边界有问题。
MCP 生态还在快速演进,入站认证已经被规范覆盖得很好,「MCP Server 如何调用下游」目前还是各家自己发挥的地带。希望我们的实践能给你一些参考。
如果你在给自己的产品做 MCP Server,可以看看 Logto 为 AI 场景提供的完整方案:Auth for AI apps, agents, and MCP servers。想看这套 token 链路的实际效果,可以直接连接 Logto MCP Server 体验。
文中涉及的技术文档: