MCP 客户端的 OAuth2 认证,以及 Identity Assertion 授权类型 | eSeGeCe 博客

MCP 客户端的 OAuth2 认证,以及 Identity Assertion 授权类型

· 组件

远程 MCP 服务器属于受保护资源,人们真正部署的服务器都位于一台授权服务器之后。到目前为止,sgcWebSockets 的 MCP 客户端只能发送 API 密钥或自定义请求头,对于静态令牌来说这没问题,但只要令牌带有有效期,一切就得由你自己负责:你要自己调用令牌端点、解析响应、盯着时间,并在令牌过期之前刷新它。

sgcWebSockets 2026.7 把这些工作搬进了组件内部。MCP 客户端现在可以使用 OAuth2 认证,它会获取访问令牌、缓存它,并在每次请求时复用,直到令牌过期为止。同一版本还新增了一种授权类型,即 Identity Assertion Authorization Grant,它可以把一个域中的身份传递到另一个域,而无需让用户再走一遍交互式登录。

是客户端,不是服务器

先澄清一点,因为这两端很容易混淆。sgcWebSockets 2026.5 发布了 MCP 服务器上的 OAuth 2.1:发现端点、PKCE 和动态客户端注册,这样基于浏览器的 MCP 客户端就能向你的 Delphi 服务器申请授权。那部分之前已经介绍过了。本文讲的是连接的另一端,也就是 MCP 客户端,它现在可以自己获取令牌,并把令牌出示给它所连接的远程 MCP 服务器。

MCP 客户端此前能做什么

两个选项,两者都仍然可用且保持不变。一个是 API 密钥,以 Authorization: Bearer <value> 的形式发送;另一个是用于租户或区域路由的自定义请求头。之前的文章 MCP Authentication Delphi 对两者都有详细介绍。

MCPClient.MCPOptions.AuthenticationOptions.ApiKey.Enabled := True;
MCPClient.MCPOptions.AuthenticationOptions.ApiKey.Value := 'YOUR_API_KEY';

MCPClient.MCPOptions.AuthenticationOptions.CustomHeader.Enabled := True;
MCPClient.MCPOptions.AuthenticationOptions.CustomHeader.Header := 'X-Tenant';
MCPClient.MCPOptions.AuthenticationOptions.CustomHeader.Value := 'Retail';

缺失的正是介于两者之间的一切:令牌端点、客户端 id 和密钥、作用域、过期时间。这正是新的 OAuth2 对象所补上的。

MCP 客户端上的 OAuth2

认证现在有了第三个分支,MCPOptions.AuthenticationOptions.OAuth2,类型为 TsgcWSMCPClientAuthenticationOAuth2_Options。它的属性正是你对一个 OAuth2 客户端所期待的那些:EnabledTokenURLClientIdClientSecretScope、一个 GrantType,以及新授权类型所使用的 IdentityAssertionOptions 对象。只有一个方法 GetBearerToken,而通常你永远不需要调用它:客户端会在每次 MCP 请求时替你调用它,并把结果放进 Authorization 请求头。

授权类型通过 GrantType 选择,其类型为 TsgcOAuth2GrantTypes

TsgcOAuth2GrantTypes = (auth2Code, auth2CodePKCE, auth2ClientCredentials,
  auth2ResourceOwnerPassword, auth2DeviceCode, auth2IdentityAssertion);

最后一个值 auth2IdentityAssertion 是 2026.7 新增的。其他几个此前已由 OAuth2 HTTP 客户端支持,现在 MCP 客户端也能用了。

客户端凭据:最常见的场景

一个与远程 MCP 服务器通信、且没有用户参与的服务,对应的就是客户端凭据授权类型。填好令牌端点和凭据,启用它,然后连接:

uses
  sgcAI, sgcAI_MCP_Client, sgcHTTP_OAuth_Types;

var
  MCPClient: TsgcWSAPIClient_MCP;
begin
  MCPClient := TsgcWSAPIClient_MCP.Create(nil);

  MCPClient.MCPOptions.HttpOptions.URL := 'https://mcp.example.com/mcp';

  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.Enabled := True;
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.GrantType := auth2ClientCredentials;
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.TokenURL := 'https://auth.example.com/oauth2/token';
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.ClientId := 'YOUR_CLIENT_ID';
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.ClientSecret := 'YOUR_CLIENT_SECRET';
  MCPClient.MCPOptions.AuthenticationOptions.OAuth2.Scope := 'mcp.read mcp.tools';

  MCPClient.Initialize;
  MCPClient.ListTools;
end;

你的代码里没有任何令牌处理逻辑。第一个需要令牌的请求会触发授权流程,之后的每个请求都复用缓存下来的令牌。

令牌缓存以及相对 API 密钥的优先级

令牌是惰性获取的,在第一个请求时取得,并连同它的过期时间保存在内存中。只要令牌仍然有效,客户端就会复用它。当它过期,或者服务器从未告诉你过期时间而令牌返回为空时,授权流程会再次执行。客户端还会稍稍提前于所声明的过期时间进行刷新,这样一个即将失效的令牌就不会被发送到一个在其到达时已经会被拒绝的请求上。

优先级很简单,值得明确说清楚:OAuth2.Enabled 为 True 时,OAuth2 优先Authorization 请求头携带的是授权流程取得的 bearer 令牌,即使 ApiKey 也处于启用状态也会被忽略。这样你可以在迁移期间保留已配置好的 API 密钥,然后只需切换 OAuth2.Enabled,其他什么都不用改。CustomHeader 与之互不影响,两种情况下都照常发送。

MCP 客户端上的 TLS 选项

MCP 客户端的 HTTP 传输层现在通过 MCPOptions.HttpOptions.TLSOptions 暴露了自己的 TLS 配置,因此与 MCP 服务器以及与令牌端点的连接都可以固定为你的环境所要求的设置,而不必使用默认值。

MCPClient.MCPOptions.HttpOptions.TLSOptions.Version := tls1_3;
MCPClient.MCPOptions.HttpOptions.TLSOptions.VerifyCertificate := True;
MCPClient.MCPOptions.HttpOptions.TLSOptions.RootCertFile := 'C:\certs\ca-bundle.pem';
MCPClient.MCPOptions.HttpOptions.TLSOptions.IOHandler := iohOpenSSL;   // or iohSChannel on Windows

Identity Assertion 授权类型

现在说说有意思的部分。auth2IdentityAssertion 是 IETF 定义的一种 OAuth2 授权类型,用于在多个域之间传递身份,它属于令牌交换风格的流程。

它所解决的场景是这样的。你的应用已经持有一个令牌,可以证明用户在域 A 中的身份,通常是用户登录时由身份提供方签发的 id 令牌。现在你需要调用域 B 中的一台 MCP 服务器,而它不会接受域 A 的令牌:它只信任自己的授权服务器,不信任别人的。用户就在那儿,而且已经通过认证,所以让他再走一遍交互式登录,正是你想避免的那种摩擦。

Identity Assertion 授权类型就是出路。你把域 A 的令牌作为 subject token 出示,交换过程会返回一个针对你所请求的 audienceresource 的令牌,而这个令牌是域 B 接受的。这也是这些选项之所以是现在这个形状的原因:SubjectTokenSubjectTokenType 描述你所持有的东西,RequestedTokenTypeAudienceResource 描述你想要的东西,而 ActorToken / ActorTokenType 让你在目标方需要时指明代表用户行事的那个服务。

这是一次多步骤的交换,客户端会替你走完全程。subject token 会被发送到请求方的授权服务器(RequestingPartyTokenURL,配合 RequestingPartyClientIdRequestingPartyClientSecret),后者返回一个跨域断言。该断言随后被出示给位于 TokenURL 的资源授权服务器,由它签发 MCP 客户端实际发送的访问令牌。如果你已经从别处拿到了断言,设置 Assertion 即可跳过第一段。

with MCPClient.MCPOptions.AuthenticationOptions.OAuth2 do
begin
  Enabled := True;
  GrantType := auth2IdentityAssertion;

  // resource authorization server (domain B): issues the token the MCP server accepts
  TokenURL := 'https://auth.domain-b.com/oauth2/token';
  ClientId := 'YOUR_CLIENT_ID';
  ClientSecret := 'YOUR_CLIENT_SECRET';
  Scope := 'mcp.tools';

  // the identity you already hold in domain A
  IdentityAssertionOptions.SubjectToken := vIdTokenFromDomainA;
  IdentityAssertionOptions.SubjectTokenType := 'urn:ietf:params:oauth:token-type:id_token';
  IdentityAssertionOptions.RequestedTokenType := 'urn:ietf:params:oauth:token-type:id-jag';

  // where the exchange happens, and who you are exchanging as
  IdentityAssertionOptions.RequestingPartyTokenURL := 'https://auth.domain-a.com/oauth2/token';
  IdentityAssertionOptions.RequestingPartyClientId := 'DOMAIN_A_CLIENT_ID';
  IdentityAssertionOptions.RequestingPartyClientSecret := 'DOMAIN_A_CLIENT_SECRET';

  // what the resulting token is for
  IdentityAssertionOptions.Audience := 'https://mcp.domain-b.com';
  IdentityAssertionOptions.Resource := 'https://mcp.domain-b.com/mcp';
end;

MCPClient.Initialize;

从调用方代码看,仍然只是一个属性加一次连接。上面这一切都发生在 GetBearerToken 内部,得到的访问令牌会像其他令牌一样被缓存起来。

跟踪整个交换过程

这一授权类型并不局限于 MCP。TsgcHTTPComponentClient_OAuth2 直接支持它,当你需要看清每一步、记录日志,或者在别处复用令牌时,这正是你想要的。在 OAuth2Options.GrantType 上选择授权类型,填好 IdentityAssertionOptions,然后调用 Start。中间产生的断言会保留在只读的 IdentityAssertion 属性中,如果你想显式地驱动整个流程,还有 DoIdentityAssertionGrant 可用。

事件会报告每一段过程。OnBeforeTokenExchangeOnAfterTokenExchangeOnErrorTokenExchange 覆盖跨域交换,而已有的 OnAuthTokenOnAuthTokenError 报告最终结果。

uses
  sgcHTTP_OAuth2_Client, sgcHTTP_OAuth_Types;

procedure TForm1.OnAfterTokenExchange(Sender: TObject; const Access_Token,
  Issued_Token_Type, Token_Type, Expires_In, Scope, RawParams: String;
  var Handled: Boolean);
begin
  // ... leg 1 done: we now hold the cross-domain assertion
  Log('assertion issued, type: ' + Issued_Token_Type);
end;

procedure TForm1.OnAuthToken(Sender: TObject; const TokenType, Token,
  Data: String);
begin
  // ... leg 2 done: this is the token domain B accepts
  Log('access token: ' + TokenType + ' ' + Token);
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  oOAuth2: TsgcHTTPComponentClient_OAuth2;
begin
  oOAuth2 := TsgcHTTPComponentClient_OAuth2.Create(nil);
  try
    oOAuth2.OAuth2Options.GrantType := auth2IdentityAssertion;
    oOAuth2.OAuth2Options.ClientId := 'YOUR_CLIENT_ID';
    oOAuth2.OAuth2Options.ClientSecret := 'YOUR_CLIENT_SECRET';
    oOAuth2.AuthorizationServerOptions.TokenURL := 'https://auth.domain-b.com/oauth2/token';

    oOAuth2.IdentityAssertionOptions.SubjectToken := vIdTokenFromDomainA;
    oOAuth2.IdentityAssertionOptions.SubjectTokenType := 'urn:ietf:params:oauth:token-type:id_token';
    oOAuth2.IdentityAssertionOptions.RequestedTokenType := 'urn:ietf:params:oauth:token-type:id-jag';
    oOAuth2.IdentityAssertionOptions.RequestingPartyTokenURL := 'https://auth.domain-a.com/oauth2/token';
    oOAuth2.IdentityAssertionOptions.Audience := 'https://mcp.domain-b.com';

    oOAuth2.OnAfterTokenExchange := OnAfterTokenExchange;
    oOAuth2.OnErrorTokenExchange := OnErrorTokenExchange;
    oOAuth2.OnAuthToken := OnAuthToken;
    oOAuth2.OnAuthTokenError := OnAuthTokenError;

    oOAuth2.Start;
  finally
    oOAuth2.Free;
  end;
end;

如果某一段失败,错误事件会按照授权服务器返回的原样携带 OAuth2 错误码和描述,因此配置错误的 audience 或不被接受的 subject token 类型会立刻显现出来,而不是稍后以 MCP 服务器返回的 401 的形式浮现。

可用性

MCP 客户端上的 OAuth2 认证、Identity Assertion 授权类型以及 MCP 客户端 TLS 选项,均已在 sgcWebSockets 2026.7 中提供,支持 Delphi 7 至 13 以及 C++Builder,覆盖 Win32/Win64、Linux64、macOS、Android 和 iOS。现有代码不受任何影响:在你设置 Enabled 之前 OAuth2 处于关闭状态,API 密钥和自定义请求头认证的行为与以往完全一致。

拥有有效订阅的客户可以从客户专区下载新版本。试用用户可以在 esegece.com/products/websockets/download 获取更新后的安装程序。

有疑问、建议,或者需要帮助把某种授权类型接入你的应用?联系我们,回复你的将是编写这些代码的人。