首页 / 文章

MCP网关的双重OAuth边界:保护用户身份安全

MCP网关不应将所有调用都集中到同一个服务账户中。通过入站验证以及代表用户进行的OAuth交互,可以保持每个用户的身份完整性。

发布者:REACTAPP.TOP阅读约 11 分钟2444 词

来源: Solving the Identity Termination Problem in MCP Gateway Architectures

位于MCP驱动的代理与企业系统之间的网关应设置两个OAuth边界,这样在代理访问的每个系统中,发起请求的人都能被识别出来。

故障案例

来看一个典型的故障场景:一名员工通过MCP网关请求代理为同事提交工单。网关首先在企业身份提供商处验证该员工的身份,确认令牌的有效性,然后将请求传递给ITSM API,由后者创建工单。一个月后,审计人员查看工单存储时发现,代理提交的每份工单上显示的都是一样的名称——即网关自身的应用身份,而真正的请求人却不在记录中。

在网关之后的每一个下游系统中,都存在同样的缺陷。人力资源系统会将每一项休假申请都记在网关的服务账户名下,资产登记系统会将每一次设备转移都归功于该账户,而采购工作流则会在每份申请中将它列为审批人。身份信息在网关处就终止了,后续系统无法再获取这些信息。

MCP网关已成为大语言模型代理与企业记录系统之间的标准桥梁,但大多数实际部署仍存在一个未明确的问题:究竟是哪个身份在授权每一次下游调用?目前主要有两种不良做法。

被终止的身份认证。在确认来电者身份后,网关会以一个统一的服务主体身份与所有后端系统通信。无论其对IT系统、人力资源系统或采购系统执行何种操作,都归因于该服务账户,而非发起请求的人。这样一来,审计追踪便失去作用,最小权限原则也会被打破。一个恶意指令即可让网关对其能够访问的任何后端系统行使全部控制权。

信任客户端。网关会原封不动地接收任何传入的承载令牌,并将其转发给下游API,而不对令牌的发行方、适用对象或范围进行任何验证。因此,只要在令牌传递路径上的任何环节发生令牌被盗用,就会悄无声息地授权任意下游调用。

对于企业级代理人工智能而言,这两种方式均不可接受,尤其是在 NIST AI 风险管理框架的治理功能要求下,该框架要求在针对受监管系统的自动化操作中保持个体责任可追溯性。

提案内容

该设计具有两个 OAuth 边界。在客户端端,入口边界会提取并验证委托访问令牌,即为调用用户针对网关的访问对象而颁发的 OAuth 2.0 携带令牌(RFC 6749)。出口边界则按照 RFC 8693 令牌交换机制进行代理交换,或在信任链允许的情况下直接进行委托转发,并为下游访问对象生成仍保留原始用户身份的令牌。

工具授权是在边界处根据解析后的令牌声明来决定的,而非嵌入在工具实现中。这样一来,身份信息的传递就成了网关的任务,而非每个工具或后端都必须记住的事情。

模式:委托边界 OAuth

简而言之:网关使用两个 OAuth 边界而非一个,而用户的身份信息则是在这两个边界之间传递的契约。

入口边界。承载令牌提取、发行方与受众验证以及声明解析均在网关的同一层完成,通常是在请求中间件或类似FastMCP的依赖注入器中,这些组件会在每个工具函数执行之前被调用。正是在这里回答了“谁在调用,他们是否有权限使用此工具?”这一问题,任何工具实现都不得跳过这一环节。作用域与工具的映射是声明式的:每个已公开的工具都会指定其所需的OAuth作用域,若令牌中缺少这些作用域,边界就会拒绝运行该工具。这种拒绝属于结构化的授权失败,而非因深层权限检查耗费过多计算资源而产生的5xx错误。

出站边界。下游调用是通过标识原始用户的令牌进行身份验证的,而非使用网关自身的服务身份。实现这一目标有两种机制:一种是由代理交换机构(如 Microsoft Entra OBO、AWS Cognito 身份池交换机构或自定义的 STS 交换机构)向下游 API 发放新的令牌,该令牌的主体仍为人类调用者;另一种是直接转发,仅适用于与网关有明确信任协议的下游服务,此时会原封不动地传递入站令牌。正是这种交换机制使得身份传递的可验证性成为现实,而不仅仅是期望而已。

中间状态。令牌被视为仅存在于单个请求中的状态。不会在多个请求之间缓存任何内容,也不会保存在会话中,更不会共享可变状态。每次调用MCP工具时都会从当前请求头中重新读取令牌,且请求完成后即将其丢弃。

实现方式

接收端:提取委托令牌

接收端边界是一个轻量级的中间件,会在每个工具函数执行之前运行。在基于FastMCP风格框架构建的Python MCP服务器中,可通过CurrentHeaders注入机制,利用一个简单的工具函数来获取承载令牌:

from typing import Optional

def extract_bearer_token(headers: dict) -> str:
    auth = headers.get("authorization") or headers.get("Authorization")
    if not auth or not auth.startswith("Bearer "):
        raise AuthError("missing or malformed bearer token")
    return auth[len("Bearer "):]

接下来是根据网关预期的发行方和受众,以及身份提供方公开的签名密钥(JWKS),对该令牌进行验证:

from jose import jwt

def validate_inbound_token(token: str, jwks: dict, audience: str, issuer: str) -> dict:
    claims = jwt.decode(
        token, jwks,
        audience=audience,
        issuer=issuer,
        options={"require_exp": True, "require_iat": True},
    )
    return claims  # sub, scp/scope, exp, etc.

验证必须在处理过程中完成。在信任关系较为简单的架构中,将 JWT 验证外包给下游代理是可行的,但这会使得网关无法在工具执行副作用之前强制实施作用域与工具的映射关系。

出站:代表交换机执行

出站流程会将入站的用户令牌替换为针对下游接收方的令牌。正是这种替换使得该模式有别于简单的转发方式,因为下游 API 永远无法直接看到入站令牌。网关会使用 urn:ietf:params:oauth:grant-type:jwt-bearer 请求(Entra)或 assume-role-with-web-identity 请求(AWS)调用身份提供方的令牌端点,并将入站令牌作为主体断言传递过去。身份提供方会为下游 API 返回一个新的令牌,其 sub 字段仍为原始用户信息。

def exchange_obo(inbound_token: str, downstream_audience: str) -> str:
    response = requests.post(
        f"{IDP_TOKEN_ENDPOINT}",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "client_id": GATEWAY_CLIENT_ID,
            "client_secret": GATEWAY_CLIENT_SECRET,
            "assertion": inbound_token,
            "scope": f"{downstream_audience}/.default",
            "requested_token_use": "on_behalf_of",
        },
        timeout=5,
    )
    response.raise_for_status()
    return response.json()["access_token"]

随后,该交换得到的令牌会被附加到出站的 HTTP 请求中:

def call_downstream(downstream_token: str, path: str) -> dict:
    return requests.get(
        f"{DOWNSTREAM_BASE_URL}{path}",
        headers={"Authorization": f"Bearer {downstream_token}"},
        timeout=10,
    ).json()

作用域映射与身份传播

工具函数在注册时会声明所需的作用域,边界机制会在工具主体执行前对这些作用域进行验证:

@tool(required_scopes=["ticketing.write"])
def open_ticket(headers: CurrentHeaders, summary: str, body: str) -> dict:
    token = extract_bearer_token(headers)
    claims = validate_inbound_token(token, jwks, audience=GATEWAY_AUD, issuer=ISSUER)
    enforce_scopes(claims, required=["ticketing.write"])  # central, not tool-local
    downstream_token = exchange_obo(token, downstream_audience=TICKETING_AUD)
    return call_downstream(downstream_token, path="/tickets")

由于范围映射是声明式的,因此可以对其进行检查。任何审计网关的人只需从注册表中获取所有工具及其所需的范围,无需阅读工具代码。

不同云服务提供商的入站边界是相同的。只有出站交换连接方式有所不同,而下面介绍的两种实现方式在整体结构上是一致的。

在 AWS 上:API Gateway、Lambda 和 Cognito

在 AWS 中,入站边界可以是 Amazon API Gateway 上的自定义 Lambda 授权器。它会将令牌与 Amazon Cognito 用户池进行验证,然后返回策略文档以及解析后的声明上下文映射,这样 MCP 服务器的 Lambda 就可以直接从 event.requestContext.authorizer 中获取声明,而无需再次验证。对于出站方向,sts:AssumeRoleWithWebIdentity 会使用入站令牌(或由 Cognito 身份池颁发的令牌),生成与用户联合身份绑定的临时 AWS 凭证。其后面的任何 API Gateway 端点,或是信任 Cognito JWT 的外部 API,都可以在 sub 声明中看到该用户信息。

在 gateway Lambda 中,Cognito 与 STS 之间的最小交互流程如下:

import boto3

def aws_obo(inbound_token: str, role_arn: str) -> dict:
    sts = boto3.client("sts")
    creds = sts.assume_role_with_web_identity(
        RoleArn=role_arn,
        RoleSessionName="mcp-gateway-obo",
        WebIdentityToken=inbound_token,
        DurationSeconds=900,
    )
    return creds["Credentials"]

所假设角色的信任策略将 Cognito 用户池指定为联合主体,同时还会检查传入令牌的 aud 声明,这意味着只有为该网关生成的令牌才能承担该角色。

在 Azure 中:Entra on-behalf-of 流程

在 Azure 中,入口边界会使用网关的应用程序 ID URI 作为受众,将传入的令牌与网关的 Entra 应用程序注册信息进行比对。对于出站请求,则会执行针对下游 API 自身应用程序注册的 Entra on-behalf-of 交换操作:

import msal

def entra_obo(inbound_token: str, downstream_scope: str) -> str:
    app = msal.ConfidentialClientApplication(
        client_id=GATEWAY_APP_ID,
        client_credential=GATEWAY_CLIENT_SECRET,
        authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    )
    result = app.acquire_token_on_behalf_of(
        user_assertion=inbound_token,
        scopes=[downstream_scope],
    )
    if "access_token" not in result:
        raise AuthError(result.get("error_description", "obo exchange failed"))
    return result["access_token"]

下游 API 必须能够接收那些受众为其自身 Entra 应用注册信息的令牌。其授权逻辑会读取 oid(对象 ID)或 upn(用户主体名称)字段,从而将调用关联到原始用户。两种模式的结构完全相同,仅有身份提供方的连接方式有所差异。

与日志记录及评估的关联

当对传播而来的身份信息进行记录时,其价值才能最大程度体现。如果网关在每次工具调用时都记录下发起请求的用户,那么审计就可以将某个代理操作追溯到具体个人。同一字段也可以被纳入评估记录中,从而将测试运行与系统所响应的行为主体联系起来。应将此模式提取出的身份信息视为系统生成的每一条审计记录和评估记录的第一列。

该模式适用的情况与不适用的情况

适用的情况包括:

  • 网关面向那些需要通过个人身份进行授权、审计或合规性控制的系统,例如IT服务管理、人力资源、财务以及受监管的工作流程。
  • 企业身份提供商支持基于标准的身份交换机制:Entra OBO、带有STS Web身份功能的Cognito,或是Auth0与Okta的令牌交换功能。
  • 下游API位于与网关相同的信任范围内,可被配置为接受用户身份令牌。
  • 该部署方式需符合NIST AI RMF、SOX、HIPAA或类似要求对自动化操作进行用户级责任追溯的治理标准。

以下情况不适用此方案:

  • 所有下游调用显然都与特定用户无关,例如仅用于读取的参考数据,且不会产生任何可归因于用户的副作用。
  • 由于已有的可信下游代理已为网关实现了完整的委托语义处理,因此网关自身的功能会变得重复。在这种情况下,网关只需构建入站边界以及出站端的委托信任版本,在明确的信任协议下直接转发数据,并承认该代理才是真正的安全边界。
  • 某个下游API拒绝接受用户访问令牌,且目前也不考虑对其进行修改。在这种情况之下,应推迟采用该模式,直到相关设置发生变化。暂时采用服务主体作为替代方案应当是经过深思熟虑的决定,并明确记录相关风险,而非默认选项。
  • 运营方面的考虑:

    • 延迟问题。每次下游调用都会导致需要额外一次访问令牌端点。只要传入的令牌有效,就可以保留已交换的令牌,其标识为(sub, downstream_audience),且这些令牌不得在不同用户之间共享。
    • 身份提供方限制。令牌端点的配额并非随意设定,需预留足够空间并监控交换失败率。
    • 令牌刷新。代理会话的持续时间往往超过单个令牌的有效期,因此需要规划令牌的轮换机制,或者接受代理会在令牌过期后停止工作的情况,而这通常也是更合理的安全做法。
    • 服务账户备用方案。对于无需用户参与的操作,如健康检查或定时后台同步,应设置专门的服务账户交换路径,该路径需有明确限制、范围较小且需进行日志记录。

    该模式的局限性

    它假设身份提供方支持基于标准的信息交换机制,但许多旧版身份提供方并不具备此功能。在那些环境中,系统只能通过书面信任协议与每个下游服务约定,直接传递令牌。这种方式适用于处于网关信任范围内的后端系统,但当下游服务由其他团队或第三方供应商运行时则不够可靠。正确的迁移方式是升级身份提供方,而非长期依赖转发机制。

    它还假设下游服务能够接受用户身份令牌。许多企业系统出现时还没有委托授权机制,仅能通过服务主体进行身份验证。在这种情况下,必须等待服务升级或在其前端引入具备身份识别功能的代理才能采用该模式。在迁移过程中,部分调用使用用户属性进行身份验证,而部分调用使用服务属性进行验证,这是正常现象,但应将其视为需要设定完成日期的技术债务,而非标准化方案。

    最后,它并未解决“作为执行主体的智能体”的责任问题。在跨网关跳转时保持人类身份的完整性确实是首要的控制措施,但大型语言模型智能体仍可能被引导执行用户从未明确要求过的操作。该模式只能确保行为的归属,无法确保行为的意图。因此需要额外的控制措施:对高影响操作进行人类确认,在接入边界设置最小权限范围,并为每次调用记录证据。

    采用检查清单

    • 将接入协议(发起方、接收方、支持的权限范围)以版本化接口的形式文档化,并像管理公共API一样对其进行管理。
    • 在工具注册时以声明式字段的形式实现权限范围与工具的映射,而非在工具代码中零散地添加临时检查逻辑。
    • 为每个下游后端在企业身份提供方处设置代理式交换机制。将代理式交换设为默认选项,而对于处于网关明确信任边界内的后端,则保持直接转发方式。
    • 在入站令牌的有效期内,以用户主体和下游受众作为键值来缓存交换得到的令牌,绝不允许在不同用户之间共享这些令牌。
    • 在每次工具调用时,都要记录入站的sub值和出站的sub值,以便对整个令牌传递过程进行全程审计。
    • 允许使用一个范围严格限定的服务账户路径来处理无需关联用户的工作,并且每次使用该路径时都必须经过审批。
    • 将那些拒绝接受用户受众令牌的下游API视为架构缺陷,并努力消除这一限制。

    随着美国企业和联邦项目开始采用NIST人工智能风险管理框架,其治理功能要求在针对受监管系统执行自动化操作时仍需明确个人责任。在网关处就终止身份认证的做法与这一要求背道而驰,会在SOX、HIPAA和FedRAMP等环境中引发审计问题,因为在这些环境中,每一次自动化状态变更都应能追溯到具体责任人。

    延伸阅读

    其基础是IETF制定的RFC 6749(OAuth 2.0授权框架)和RFC 8693(OAuth 2.0令牌交换规范)。供应商文档则涵盖了Microsoft身份平台的OAuth 2.0“代表他人”流程、AWS STS的AssumeRoleWithWebIdentity API参考信息以及Amazon Cognito身份池的相关内容。NIST发布了人工智能风险管理框架(AI RMF 1.0),而MCP规范中也包含了一个授权部分,值得与上述所有资料一并阅读。