Eine Dual-OAuth-Grenze für MCP-Gateways: Schutz der Benutzeridentität
MCP-Gateways sollten nicht jeden Anruf in ein einziges Service-Konto leiten. Die eingehende Validierung zusammen mit einem OAuth-Austausch im Auftrag des Benutzers bewahrt die Identität jedes Benutzers.
Quellen: Solving the Identity Termination Problem in MCP Gateway Architectures
Ein Gateway, das zwischen einem von MCP gesteuerten Agenten und Unternehmenssystemen steht, sollte zwei OAuth-Grenzen aufweisen, damit die Person, die die Anfrage gestellt hat, in jedem System, das der Agent berührt, identifizierbar bleibt.
Der Fehler
Betrachten wir einen typischen Fehlerfall. Ein Mitarbeiter bittet einen Agenten über ein MCP-Gateway, ein Ticket für einen Kollegen zu erstellen. Das Gateway überprüft den Mitarbeiter beim Unternehmens-Identitätsanbieter, validiert das Token und leitet die Anfrage an die ITSM-API weiter, die anschließend das Ticket erstellt. Ein Monat später öffnet ein Prüfer den Ticket-Store und sieht bei allem, was der Agent eingereicht hat, nur einen Namen: die eigene Anwendungsidentität des Gateways. Die tatsächlichen Anfragenden fehlen in den Aufzeichnungen.
Derselbe Fehler tritt in jedem nachgelagerten System hinter dem Gateway auf. Das HR-System ordnet jeden Antrag auf Urlaub dem Servicekonto des Gateways zu, das Asset-Register ordnet jeden Geräteaustausch dorthin zu, und der Beschaffungsworkflow listet es als Genehmiger für jede Anfrage auf. Die Identität endet am Gateway, und nichts weiter kann sie wiederherstellen.
MCP-Gateways sind zur Standardbrücke zwischen LLM-Agenten und Unternehmenssystemen geworden, doch die meisten Produktivimplementierungen lassen eine Frage offen: Wessen Identität autorisiert jeden nachgelagerten Aufruf? Zwei Anti-Muster dominieren hier.
Beendete Identität. Nachdem überprüft wurde, wer anruft, kommuniziert das Gateway mit jedem Backend als ein gemeinsamer Service-Principal. Was auch immer es mit dem IT-System, dem HR-System oder dem Beschaffungssystem macht, wird seinem Service-Konto zugeschrieben und nicht der Person hinter der Anfrage. Audit-Spuren werden nutzlos und das Prinzip der geringsten Berechtigungen bricht zusammen. Ein fehlerhaftes Prompt kann die volle Reichweite des Gateways gegenüber jedem Backend nutzen, das es erreichen kann.
Vertrauen Sie dem Client. Das Gateway akzeptiert jedes eingehendeBearer-Token und leitet es unverändert an die nachgelagerten APIs weiter, ohne den Aussteller, die Zielgruppe oder den Umfang zu überprüfen. Ein Diebstahl des Tokens irgendwo auf dem Kommunikationsweg autorisiert somit stillschweigend willkürliche Anrufe an nachgelagerte Systeme.
Auch diese Ansätze sind für unternehmensweite Agenten-KI unzulässig, insbesondere im Rahmen der Govern-Funktion des NIST AI Risk Management Frameworks, die eine Aufrechterhaltung der individuellen Verantwortlichkeit bei automatisierten Aktionen an regulierten Systemen vorsieht.
Der Vorschlag
Die Architektur weist Zwei OAuth-Grenzen auf. An der Client-Seite prüft die eingehende Grenze einen delegierten Zugriffstoken, also einen OAuth 2.0-Bearer-Token (RFC 6749), der dem aufrufenden Benutzer für die Zielgruppe der Gateway-Instanz ausgestellt wurde. Die ausgehende Grenze führt einen Austausch im Namen des Gateways durch (im Einklang mit dem Token-Austausch nach RFC 8693) oder, sofern die Vertrauenskette es zulässt, eine direkte Delegation, und erzeugt anschließend einen Token für die nachgelagerte Zielgruppe, der weiterhin die Identität des ursprünglichen Benutzers enthält.
Die Autorisierung der Tool wird an der Grenze auf der Grundlage der analysierten Token-Ansprüche entschieden und nicht innerhalb der Tool-Implementierungen festgelegt. Dadurch ist es Aufgabe des Gateways, die Identität weiterzuleiten, anstatt dass jedes Tool oder Backend sie sich merken muss.
Das Muster: Delegiertes Grenz-OAuth
Kurz gesagt: Das Gateway verwendet zwei OAuth-Grenzen statt nur einer, und die Identität des Benutzers ist der Vertrag, der zwischen ihnen übertragen wird.
Die Eingangsgrenze. Die Extraktion des Träger-Tokens, die Überprüfung von Aussteller und Empfänger sowie die Analyse der Angaben finden alle in einer Schicht des Gateways statt – typischerweise im Request-Middleware oder in einem fastmcp-ähnlichen Abhängigkeitsinjektor – und werden vor jeder Tool-Funktion ausgeführt. Hier wird beantwortet, „wer ruft an und darf er dieses Tool verwenden?“, und keine Tool-Implementierung darf diesen Schritt überspringen. Die Zuordnung von Scope zu Tool erfolgt deklarativ: Jedes bereitgestellte Tool nennt die OAuth-Scope, die es benötigt, und die Grenze weigert sich, es auszuführen, wenn das Token diese Scope nicht enthält. Die Ablehnung ist ein strukturiertes Autorisierungsversagen, kein 5xx-Fehler aufgrund einer tief im Stack liegenden Berechtigungsprüfung, die dabei Rechenleistung verbraucht hat.
Die Ausgangsgrenze. Der nachgelagerte Aufruf wird durch ein Token authentifiziert, das den ursprünglichen Benutzer angibt, niemals durch die eigene Service-Identität der Gateway-Instanz. Dazu werden zwei Mechanismen genutzt. Ein auf Veranlassung des Exchanges erstelltes Token (Microsoft Entra OBO, ein AWS Cognito Identity-Pool-Exchange oder ein benutzerdefiniertes STS-Exchange) wird an die nachgelagerte API gesendet, wobei der menschliche Anrufer weiterhin als Empfänger angegeben wird. Die direkte Weiterleitung, die ausschließlich für nachgelagerte Dienste vorgesehen ist, die über eine ausdrückliche Vertrauensvereinbarung mit dem Gateway verfügen, überträgt das eingehende Token unverändert weiter. Der Exchange sorgt dafür, dass die Übertragung nachweisbar ist und nicht nur erwartet werden kann.
Zwischenzeitlich. Tokens gelten als Zustand, der nur für eine Anfrage gilt. Nichts wird über Anfragen hinweg im Cache gespeichert, nichts wird in einer Sitzung beibehalten, und kein veränderlicher Zustand wird geteilt. Jeder MCP-Toolaufruf liest das Token erneut aus den aktuellen Header-Informationen und vergisst es, sobald die Anfrage abgeschlossen ist.
Implementierung
Eingang: Extrahieren des delegierten Tokens
Die Eingangsgrenze besteht aus einem einfachen Middleware-Modul, das vor jeder Tool-Funktion ausgeführt wird. In einem Python-MCP-Server, der auf einem FastMCP-ähnlichen Framework basiert, verlassen Sie sich auf eine CurrentHeaders-Injektion und holen Sie das Token mit einer kleinen Hilfsfunktion ab:
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 "):]
Danach folgt die Überprüfung dieses Tokens gegenüber dem Aussteller und dem Empfänger, die das Gateway erwartet, unter Verwendung der veröffentlichten Signierungschlüssel des Identitätsanbieters (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.
Die Validierung muss im Prozess selbst stattfinden. Die Auslagerung der JWT-Validierung an einen nachgelagerten Proxy kann in engen Vertrauenstopologien funktionieren, entzieht dem Gateway jedoch die Möglichkeit, eine Zuordnung von Scope zu Tool vor dem Ausführen der Nebeneffekte des Tools durchzusetzen.
Ausgehende Kommunikation: im Auftrag des Exchanges
Bei der ausgehenden Kommunikation wird das eingehende Benutzer-Token gegen ein Token ausgetauscht, das an die nachgelagerte Zielgruppe gerichtet ist. Genau dieser Austausch unterscheidet dieses Vorgehen von einer einfachen Weiterleitung, da die nachgelagerte API das eingehende Token niemals direkt sieht. Das Gateway ruft den Token-Endpunkt des Identitätsanbieters mit einer urn:ietf:params:oauth:grant-type:jwt-bearer-Anfrage (Entra) oder einer assume-role-with-web-identity-Anfrage (AWS) auf und übermittelt dabei das eingehende Token als Subjektbehauptung. Der Anbieter gibt ein neues Token für die nachgelagerte API zurück, dessen sub-Claim weiterhin auf den ursprünglichen Benutzer verweist.
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"]
Das ausgetauschte Token wird anschließend der ausgehenden HTTP-Anfrage hinzugefügt:
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()
Scope-Kartierung und Identitätsverbreitung
Die Funktionen eines Tools deklarieren bei der Registrierung ihre erforderlichen Scopes, und die Grenzkontrolle stellt sicher, dass diese eingehalten werden, bevor der Tool-Code ausgeführt wird:
@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")
Weil die Skopuskartierung deklarativ ist, kann sie überprüft werden. Jeder, der das Gateway auditiert, erhält alle Tools sowie die dafür erforderlichen Skope allein aus dem Registry, ohne den Tool-Code lesen zu müssen.
Die Eingangsgrenze ist bei allen Cloud-Anbietern dieselbe. Nur die Ausgangsverbindungen unterscheiden sich, und die beiden unten beschriebenen Implementierungen weisen eine gemeinsame Gesamtstruktur auf.
Auf AWS: API Gateway, Lambda und Cognito
In AWS kann die Eingangsprüfung durch einen benutzerdefinierten Lambda-Autorisator in Amazon API Gateway erfolgen. Dieser überprüft den Token gegen einen Amazon Cognito-User-Pool und gibt ein Policy-Dokument sowie eine Kontextkarte mit den analysierten Angaben zurück, sodass der Lambda-Server des MCP die Angaben direkt aus event.requestContext.authorizer abrufen kann, anstatt erneut zu prüfen. Auf der Ausgangsseite verwendet sts:AssumeRoleWithWebIdentity den eingehenden Token (oder einen von einem Cognito-Identity-Pool ausgestellten Token) und liefert temporäre AWS-Zugangsdaten, die mit der federierten Identität des Benutzers verknüpft sind. Jeder dahinterliegende API-Gateway-Endpunkt oder eine externe API, die Cognito-JWTs vertraut, erkennt den Benutzer dann anhand der sub-Angabe.
Ein minimales Austauschverfahren zwischen Cognito und STS im Gateway-Lambda sieht wie folgt aus:
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"]
Die Vertrauensrichtlinie der angenommenen Rolle bezeichnet den Cognito-User-Pool als federierten Prinzipal und überprüft außerdem den aud-Claim des eingehenden Tokens, was bedeutet, dass nur ein für dieses Gateway erstelltes Token die Rolle übernehmen kann.
Auf Azure: Der Entra on-behalf-of-Flow
Auf Azure prüft die eingehende Grenze das ankommende Token anhand der Entra-App-Registrierung des Gateways, wobei die Application ID URI des Gateways als Zielgruppe verwendet wird. Bei der Ausgangsverbindung wird ein Entra on-behalf-of-Austausch gegenüber der eigenen App-Registrierung der nachgelagerten API durchgeführt:
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"]
Die downstream API muss Tokens akzeptieren, deren Zielgruppe ihre eigene Entra-App-Registrierung ist. Ihre Autorisierungslogik liest den oid (Objekt-ID) oder upn (Benutzer-Principal-Name), um den Aufruf dem ursprünglichen Benutzer zuzuordnen. Die Struktur des Musters bleibt identisch, nur die Verbindung zum Identitätsanbieter ändert sich.
Verknüpfung mit Protokollierung und Bewertung
Die Verbreitung der Identität ist am nützlichsten, wenn sie aufgezeichnet wird. Wenn das Gateway den eingehenden Benutzer bei jeder Tool-Aufrufprotokollierung erfasst, kann eine Prüfung eine Agentenaktion auf eine Person zurückverfolgen. Dasselbe Feld kann in die Bewertungsprotokolle übernommen werden, wodurch Testläufe mit den Personen verknüpft werden, deren Verhalten das System bewertet. Betrachten Sie die von diesem Muster extrahierte Identität als erste Spalte in jedem Prüf- und Bewertungsprotokoll, das das System erzeugt.
Wann dieses Muster passt und wann nicht
Es passt, wenn:
- Das Gateway verbindet Systeme, in denen die individuelle Identität für Autorisierungen, Audits oder Konformitätsprüfungen von Bedeutung ist, wie beispielsweise IT-Serviceverwaltung, Personalwesen, Finanzen und regulierte Arbeitsabläufe.
- Der Unternehmens-Identitätsanbieter unterstützt Standards für den Austausch von Identitätsinformationen: Entra OBO, Cognito mit STS-Web-Identität oder Auth0 sowie Okta für den Tokenaustausch.
- Die nachgelagerten APIs befinden sich innerhalb derselben Vertrauensgrenze wie das Gateway und können so konfiguriert werden, dass sie Benutzer-Tokens akzeptieren.
- Die Implementierung unterliegt Vorgaben wie NIST AI RMF, SOX, HIPAA oder ähnlichen Governance-Regelungen, die eine Rechenschaftspflicht auf Benutzerebene für automatisierte Aktionen erfordern.
Es eignet sich nicht, wenn:
- jeder nachgelagerte Aufruf eindeutig nicht benutzerbezogen ist, beispielsweise Lesedaten ohne benutzerbezogene Nebeneffekte.
- Ein vertrauenswürdiger downstream-Proxy stellt bereits eine vollständige delegierte Semantik für das Gateway bereit, wodurch die eigentliche Arbeit des Gateways überflüssig wird. Hier erstellt das Gateway lediglich die eingehende Grenze sowie eine delegierte-trust-Variante der ausgehenden Seite, leitet die Anfragen direkt unter Berücksichtigung eines expliziten Vertrauensvertrags weiter und akzeptiert, dass der Proxy die eigentliche Sicherheitsgrenze darstellt.
- Eine downstream-API weist Benutzer-Audience-Tokens ab, und eine Anpassung dieser API ist nicht in Frage gekommen. Verschieben Sie dieses Muster, bis sich dies ändert. Der Rückgriff auf einen Service-Principal „vorerst“ sollte eine bewusste Entscheidung mit dokumentiertem Risiko sein, niemals die Standardlösung.
Operative Überlegungen:
- Latenz. Jeder Austausch über OBO kostet einen zusätzlichen Aufruf am Token-Endpoint für jede nachgelagerte Anfrage. Bewahren Sie die ausgetauschten Token so lange auf, wie das eingehende Token gültig ist – mit Schlüssel
(sub, downstream_audience)– und niemals zwischen verschiedenen Benutzern. - Drosselung durch Identitätsanbieter. Die Quoten für den Token-Endpoint sind nicht unbedeutend. Bereiten Sie Puffer vor und überwachen Sie die Fehlerraten bei Austauschvorgängen.
- Aufräumen. Lange Agentensitzungen überdauern die Gültigkeitsdauer eines einzelnen Tokens. Planen Sie eine Rotation der Refresh-Tokens oder akzeptieren Sie, dass ein Agentensatz endet, wenn sein Token abläuft – was in der Regel ohnehin die richtige Sicherheitsstrategie ist.
- Notfalllösung mit Service-Konto. Halten Sie einen Austauschweg über ein Service-Konto für wirklich benutzerlose Aufgaben bereit, wie z. B. Health Checks oder geplante Hintergrundsynchronisationen. Stellen Sie sicher, dass dieser Weg klar definiert, eng gefasst und protokolliert ist.
Wo das Muster nicht ausreicht
Es setzt voraus, dass der Identitätsanbieter einen Standard für den Austausch im Auftrag anderer unterstützt, was bei vielen älteren Anbietern nicht der Fall ist. In solchen Umgebungen weicht die Ausgangsseite auf das direkte Übergeben der Tokens zurück, wobei dafür eine schriftliche Vertrauensvereinbarung mit jedem nachgelagerten Service besteht. Das ist für Backend-Dienste innerhalb des Vertrauensbereichs des Gateways akzeptabel, reicht jedoch nicht aus, wenn ein anderes Team oder ein Drittanbieter den nachgelagerten Service betreibt. Der richtige Migrationsweg besteht darin, den Identitätsanbieter zu aktualisieren, anstatt sich dauerhaft auf das Weiterleiten zu beschränken.
Es geht außerdem davon aus, dass die nachgelagerten Dienste Benutzer-Zugriffstoken akzeptieren. Viele Unternehmenssysteme entstanden vor der Delegierten Autorisierung und authentifizieren sich nur gegenüber Dienstprinzipalen. In diesem Fall muss das Muster warten, bis der Dienst aufgerüstet wird oder ein identitätsbewusster Proxy davor eingeführt wird. Eine teilweise Umsetzung, bei der einige Aufrufe benutzerbezogen und andere dienstbezogen sind, ist während der Migration der Normalzustand, sollte jedoch als technischer Schuldenberg mit einem Fälligkeitsdatum erfasst und nicht normalisiert werden.
Letztendlich löst es das Problem der Rechenschaftspflicht des Agenten als Handelnden nicht. Die Beibehaltung der Identität des Menschen über mehrere Gateway-Übergänge stellt zwar die richtige primäre Kontrollmaßnahme dar, doch ein LLM-Agent kann dennoch dazu angeleitet werden, Aktionen auszuführen, die der Benutzer nie wörtlich angefordert hat. Das Muster garantiert lediglich die Zuordnung, nicht jedoch die Absicht. Ergänzende Kontrollmechanismen tragen diese Verantwortung: menschliche Bestätigung für hochwirksame Aktionen, minimale Berechtigungsrahmen an der Eingangsstelle sowie Protokollierung von Beweismitteln pro Aufruf.
Adoptions-Checkliste
- Dokumentieren Sie den Eingangsvertrag (Ersteller, Zielgruppe, unterstützte Berechtigungsrahmen) als versionierte Schnittstelle und verwalten Sie ihn wie eine öffentliche API.
- Implementieren Sie die Zuordnung von Berechtigungsrahmen zu Tools als deklaratives Feld bei der Registrierung der Tools und nicht als ad hoc-Tests, die im Code der Tools verteilt sind.
- Stellen Sie für jeden downstream-Backend einen On-Behalf-of-Austausch mit dem Enterprise-Identity-Provider ein. Machen Sie OBO zum Standard und behalten Sie die direkte Weiterleitung für Backends innerhalb der expliziten Vertrauensgrenze des Gateways bei.
- Cachieren Sie die ausgetauschten Tokens während der Gültigkeitsdauer des eingehenden Tokens, wobei als Schlüssel der Benutzer sowie das downstream-Publikum verwendet werden, und teilen Sie sie niemals zwischen Benutzern.
- Bei jedem Tool-Aufruf speichern Sie sowohl den eingehenden
subals auch den ausgehendensub, damit die Übertragung von Anfang bis Ende überwacht werden kann. - Erlauben Sie einen eng begrenzten Service-Account-Pfad für Arbeiten ohne Benutzer und erfordern Sie eine Genehmigung bei jeder Verwendung.
- Protokollieren Sie die downstream-APIs, die Benutzer-Publikum-Token ablehnen, als architektonisches Problem und arbeiten Sie daran, diese Einschränkung zu beseitigen.
Da US-Unternehmen und Bundesprogramme das NIST AI Risk Management Framework übernehmen, erfordert seine Govern-Funktion eine individuelle Verantwortlichkeit, damit automatisierte Aktionen an regulierten Systemen nachvollziehbar bleiben. Das Beenden der Identitätsverwaltung an der Zugangsstelle steht diesem direkt entgegen und führt in Umgebungen nach SOX, HIPAA und FedRAMP zu Prüfungsergebnissen, da jede automatische Zustandsänderung auf einen bestimmten Menschen zurückverfolgt werden sollte.
Weiterführende Literatur
Die Grundlagen bilden RFC 6749 (das OAuth 2.0 Authorization Framework) und RFC 8693 (OAuth 2.0 Token Exchange) vom IETF. Herstellerspezifikationen behandeln den OAuth 2.0 On-Behalf-Of-Flow der Microsoft Identity Platform, die AWS STS AssumeRoleWithWebIdentity API-Dokumentation sowie die Amazon Cognito Identity Pools. NIST veröffentlicht das AI Risk Management Framework (AI RMF 1.0), und die MCP-Spezifikation enthält einen Autorisierungsabschnitt, der zusammen mit all den oben genannten Quellen gelesen werden sollte.