OAuth2 voor de MCP Client, en de Identity Assertion Grant | eSeGeCe Blog

OAuth2 voor de MCP Client, en de Identity Assertion Grant

· Componenten

Remote MCP servers zijn beschermde resources, en de servers die mensen in de praktijk uitrollen staan achter een authorization server. Tot nu toe kon de sgcWebSockets MCP client een API key of een custom header versturen. Dat volstond voor een statisch token, maar het betekende dat alles met een levensduur jouw probleem was: je riep zelf het token endpoint aan, parseerde het antwoord, hield de klok in de gaten en ververste het token voordat het verliep.

sgcWebSockets 2026.7 verplaatst dat werk naar de component. De MCP client kan zich nu authenticeren met OAuth2, dus hij haalt het access token op, bewaart het in de cache en hergebruikt het bij elke request tot het verloopt. Dezelfde release voegt een nieuw grant type toe, de Identity Assertion Authorization Grant, die een identiteit van het ene domein doorgeeft aan het andere zonder de gebruiker door een tweede interactieve login te sturen.

Client, niet server

Eerst een verduidelijking, want de twee kanten worden makkelijk door elkaar gehaald. sgcWebSockets 2026.5 leverde OAuth 2.1 op de MCP Server: discovery endpoints, PKCE en dynamic client registration, zodat een MCP client in de browser kon autoriseren tegen jouw Delphi server. Dat is eerder behandeld. Dit artikel gaat over de andere kant van de lijn, de MCP Client, die nu zelf een token verkrijgt en dat aanbiedt aan de remote MCP server waarmee hij verbindt.

Wat de MCP client eerder kon

Twee opties, beide nog steeds beschikbaar en beide ongewijzigd. Een API key, verstuurd als Authorization: Bearer <value>, en een custom header voor routing per tenant of regio. Het eerdere artikel MCP Authenticatie in Delphi behandelt beide in detail.

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';

Wat ontbrak is alles daartussenin: een token endpoint, een client id en secret, een scope, een vervaltijd. Dat is wat het nieuwe OAuth2 object toevoegt.

OAuth2 op de MCP client

Authenticatie heeft nu een derde tak, MCPOptions.AuthenticationOptions.OAuth2, van het type TsgcWSMCPClientAuthenticationOAuth2_Options. De properties zijn precies wat je van een OAuth2 client verwacht: Enabled, TokenURL, ClientId, ClientSecret, Scope, een GrantType, en een IdentityAssertionOptions object dat door de nieuwe grant wordt gebruikt. Er is één methode, GetBearerToken, en normaal roep je die nooit aan: de client roept hem voor jou aan bij elke MCP request en zet het resultaat in de Authorization header.

De grant selecteer je met GrantType, van het type TsgcOAuth2GrantTypes:

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

De laatste waarde, auth2IdentityAssertion, is nieuw in 2026.7. De andere werden al ondersteund door de OAuth2 HTTP client en zijn nu ook bereikbaar vanuit de MCP client.

Client credentials: het gangbare geval

Een service die met een remote MCP server praat, zonder gebruiker in de lus, is de client credentials grant. Vul het token endpoint en de credentials in, zet hem aan en verbind:

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;

Geen tokenafhandeling in je eigen code. De eerste request die een token nodig heeft start de grant, en elke request daarna hergebruikt wat er in de cache staat.

Token caching en voorrang boven de API key

Het token wordt lazy opgehaald, bij de eerste request, en samen met de vervaltijd in het geheugen bewaard. Zolang het geldig is hergebruikt de client het. Zodra het verloopt, of wanneer de server nooit een vervaltijd meegaf en het token leeg terugkomt, draait de grant opnieuw. De client ververst ook iets vóór de opgegeven vervaltijd, zodat een token dat bijna verloopt niet wordt meegestuurd met een request die op het moment van aankomst al geweigerd zou worden.

De voorrang is eenvoudig en het is goed om die duidelijk te benoemen: als OAuth2.Enabled True is, wint OAuth2. De Authorization header draagt het bearer token uit de grant, en ApiKey wordt genegeerd, zelfs als die ook aanstaat. Zo kun je een bestaande API key geconfigureerd laten staan terwijl je migreert, daarna OAuth2.Enabled omzetten en verder niets veranderen. CustomHeader staat daar los van en wordt in beide gevallen nog steeds verstuurd.

TLS opties op de MCP client

Het HTTP transport van de MCP client stelt nu zijn eigen TLS configuratie beschikbaar via MCPOptions.HttpOptions.TLSOptions, zodat de verbinding met de MCP server en met het token endpoint kan worden vastgezet op de instellingen die jouw omgeving vereist, in plaats van op de standaardwaarden.

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

De Identity Assertion grant

Nu het interessante deel. auth2IdentityAssertion is een door de IETF gedefinieerde OAuth2 grant om een identiteit door te geven tussen domeinen, en het is een flow in de stijl van een token exchange.

De situatie die hij oplost ziet er zo uit. Jouw applicatie heeft al een token dat bewijst wie de gebruiker is in domein A, doorgaans een id token van de identity provider waarmee de gebruiker is ingelogd. Nu moet je een MCP server in domein B aanroepen, en die accepteert het token van domein A niet: hij vertrouwt zijn eigen authorization server, niet die van iemand anders. De gebruiker zit er gewoon bij en is al geauthenticeerd, dus hem door een tweede interactieve login sturen is precies de wrijving die je wilt vermijden.

De Identity Assertion grant is de uitweg. Je biedt het token van domein A aan als subject token, en de exchange geeft een token terug dat is afgebakend tot de audience of resource waar je om vroeg, een token dat domein B wel accepteert. Daarom zien de opties eruit zoals ze eruitzien: SubjectToken en SubjectTokenType beschrijven wat je hebt, RequestedTokenType, Audience en Resource beschrijven wat je wilt, en met ActorToken / ActorTokenType benoem je de service die namens de gebruiker handelt, wanneer het doel dat vereist.

Het is een exchange met meerdere stappen, en de client loopt die voor jou af. Het subject token gaat naar de authorization server van de aanvragende partij (RequestingPartyTokenURL, met RequestingPartyClientId en RequestingPartyClientSecret), die een assertion teruggeeft die over domeinen heen geldt. Die assertion wordt vervolgens aangeboden aan de resource authorization server op TokenURL, die het access token uitgeeft dat de MCP client daadwerkelijk verstuurt. Heb je de assertion al ergens anders vandaan, zet dan Assertion en de eerste stap wordt overgeslagen.

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;

Vanuit de aanroepende code blijft het één property en één connect. Alles hierboven gebeurt binnen GetBearerToken, en het resulterende access token wordt gecachet zoals elk ander.

De exchange volgen

De grant is niet beperkt tot MCP. TsgcHTTPComponentClient_OAuth2 ondersteunt hem rechtstreeks, en dat is wat je wilt als je elke stap moet zien, wilt loggen, of het token elders wilt hergebruiken. Selecteer de grant op OAuth2Options.GrantType, vul IdentityAssertionOptions in en roep Start aan. De tussenliggende assertion blijft achter in de alleen-lezen property IdentityAssertion, en DoIdentityAssertionGrant staat klaar als je de flow expliciet wilt aansturen.

De events rapporteren elke stap. OnBeforeTokenExchange, OnAfterTokenExchange en OnErrorTokenExchange dekken de exchange tussen de domeinen, en de bestaande OnAuthToken en OnAuthTokenError rapporteren het eindresultaat.

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;

Faalt een stap, dan draagt het error event de OAuth2 foutcode en beschrijving zoals de authorization server ze teruggaf, zodat een verkeerd geconfigureerde audience of een niet geaccepteerd subject token type meteen zichtbaar is in plaats van later op te duiken als een 401 van de MCP server.

Beschikbaarheid

OAuth2 authenticatie op de MCP client, de Identity Assertion grant en de TLS opties van de MCP client zijn beschikbaar in sgcWebSockets 2026.7 voor Delphi 7 tot en met 13 en C++Builder, op Win32/Win64, Linux64, macOS, Android en iOS. Er verandert niets voor bestaande code: OAuth2 staat uit tot je Enabled zet, en authenticatie met een API key of een custom header werkt precies zoals voorheen.

Klanten met een actief abonnement kunnen de nieuwe build downloaden uit het klantengedeelte. Trial gebruikers kunnen het bijgewerkte installatieprogramma ophalen op esegece.com/products/websockets/download.

Vragen, feedback of hulp nodig bij het inbouwen van een grant in je app? Neem contact op, je krijgt antwoord van de mensen die de code hebben geschreven.