Podwójna granica OAuth dla bramek MCP: Ochrona tożsamości użytkownika
Bramki MCP nie powinny łączyć wszystkich połączeń w jeden konto usługowe. Walidacja przy przychodzących żądaniach w połączeniu z wymianą danych w imieniu OAuth zapewnia zachowanie integralności tożsamości każdego użytkownika.
Źródła: Solving the Identity Termination Problem in MCP Gateway Architectures
Brama łącząca agenta sterowanego przez MCP z systemami przedsiębiorstwa powinna mieć dwa granice OAuth, aby osoba składająca żądanie pozostawała rozpoznawalna w każdym systemie, z którym ma kontakt agent.
Powód niepowodzenia
Rozważmy typowe niepowodzenie. Pracownik prosi agenta, za pośrednictwem bramy MCP, o utworzenie zgłoszenia w imieniu kolegi z zespołu. Brama sprawdza tożsamość pracownika w usłudze dostarczającej identyfikatory korporacyjne, waliduje token nośny i przekazuje żądanie do API ITSM, które tworzy zgłoszenie. Miesiąc później audytor otwiera bazę zgłoszeń i widzi na wszystkich utworzonych przez agenta dokumentach to samo nazwisko: tożsamość aplikacji samej bramy. Prawdziwi wnioskodawcy nie figurują w tych zapisach.
To samo ustępstwo występuje we wszystkich systemach poniżej bramy. System HR przypisuje każdą prośbę o urlop do konta usługowego bramy, rejestr aktywów odnotowuje każdą przenoszenie urządzenia do niego, a proces zakupowy wymienia je jako osobę zatwierdzającą każdą prośbę. Tożsamość kończy się przy bramie i nic dalej nie może jej odzyskać.
Bramy MCP stały się standardowym mostem pomiędzy agentami LLM a systemami korporacyjnymi, jednak większość wdrożeń produkcyjnych pozostawia jedno niejasne pytanie: czyja tożsamość autoryzuje każde połączenie poniżej bramy? Dominują dwa antypatrony.
Zakończona tożsamość. Po sprawdzeniu, kto dzwoni, bramka komunikuje się z każdym backendem jako jeden wspólny principal usługi. Cokolwiek zrobi w systemie IT, systemie HR czy w procesie zakupowym, przypisuje się to do jej konta usługowego, a nie do osoby wysyłającej żądanie. Ślady audytowe stają się bezużyteczne, a zasada minimalnych uprawnień przestaje funkcjonować. Nieprawidłowo działający prompt może wykorzystać pełne uprawnienia bramki wobec każdego backendu, do którego może uzyskać dostęp.
Zaufaj klientowi. Bramka przyjmuje każdy token nośny, jaki przychodzi, i przekazuje go bez zmian dalej do API, bez sprawdzania emitenta, odbiorców ani zakresu uprawnień. Kradzież tokena w dowolnym momencie na ścieżce agenta potem w tajemnicy upoważnia do dowolnych żądań skierowanych do systemów docelowych.
Żadne z tych rozwiązań nie jest przyjęte w przypadku korporacyjnych systemów AI opartych na agencjach, szczególnie zgodnie z funkcją Govern w ramach NIST AI Risk Management Framework, która wymaga zachowania indywidualnej odpowiedzialności we wszystkich automatycznych działaniach na systemach regulowanych.
Propozycja
Konstrukcja obejmuje dwa granice OAuth. Na poziomie klienta pierwszy z nich pobiera i weryfikuje token uprawnień delegowanych, czyli token typu bearer OAuth 2.0 (RFC 6749) wydany użytkownikowi wykonującemu żądanie dla grupy docelowej bramy. Drugi granic realizuje wymianę tokenów w imieniu bramy (zgodnie z zasadami RFC 8693) lub, jeśli łańcuch zaufania na to pozwala, bezpośrednie przekierowanie delegowane, tworząc token dla grupy docelowej poniżej w łańcuchu, który nadal zawiera identyfikator oryginalnego użytkownika.
Uprawnienia narzędzia są określane na granicy, na podstawie przetworzonych informacji z tokenów, a nie wewnątrz implementacji narzędzia. Dzięki temu przenoszenie tożsamości jest zadaniem bramy, a nie czymś, co każde narzędzie lub serwer tła musi pamiętać.
Wzorzec: Delegated Boundary OAuth
Krótko mówiąc: brama obsługuje dwa granice OAuth, a nie jeden, a tożsamość użytkownika stanowi umowę przekazywaną pomiędzy nimi.
Granica przychodząca. Wyodrębnianie tokena nośnego, weryfikacja wydawcy i odbiorcy oraz analiza danych tokena odbywają się w jednej warstwie bramy, zazwyczaj w middleware do obsługi żądań lub w iniektorze zależności typu FastMCP, który działa przed każdą funkcją narzędzia. To właśnie tutaj odpowiada się na pytanie „kto dzwoni i czy ma prawo korzystać z tego narzędzia?”, a żadna implementacja narzędzia nie może tego pominąć. Mapowanie zakresów na narzędzia jest deklaratywne: każde udostępnione narzędzie podaje nazwy zakresów OAuth, których potrzebuje, a granica odmawia jego uruchomienia, jeśli token ich nie zawiera. Ta odmowa stanowi ustrukturyzowany błąd autoryzacji, a nie błąd 5xx wynikający z głębokiej weryfikacji uprawnień, która zużywa zasoby obliczeniowe w trakcie działania.
Granica wyjściowa. Wywołanie docelowe jest uwierzytelniane za pomocą tokena, który wskazuje oryginalnego użytkownika, a nie tożsamości usługi bramy. Dostępne są dwa mechanizmy do osiągnięcia tego celu. Mechanizm typu on-behalf-of exchange (Microsoft Entra OBO, AWS Cognito identity-pool exchange lub dostosowany STS exchange) wydaje nowy token skierowany do API docelowego, przy czym podmiotem tego tokena pozostaje osoba dokonująca wywołania. Bezpośrednie przekierowanie, przeznaczone dla usług docelowych objętych wyraźną umową zaufania z bramą, przekazuje token wejściowy bez żadnych zmian. To właśnie ten mechanizm umożliwia udowodnienie propagacji danych, a nie jedynie jej oczekiwanie.
Pomiędzy. Tokeny są traktowane jako stan istniejący tylko przez czas jednej prośby. Nic nie jest przechowywane w pamięci między prośbami, nic nie jest zachowywane w sesji, a żaden zmienny stan nie jest udostępniany. Każda wywołanie narzędzia MCP ponownie odczytuje token z aktualnych nagłówków i zapomina o nim po zakończeniu prośby.
Implementacja
Wejście: wyodrębnianie delegowanego tokena
Grenica wejścia to prosty middleware, który uruchamia się przed każdą funkcją narzędzia. W serwerze MCP w Pythonie zbudowanym na frameworku typu FastMCP należy polegać na iniekcji CurrentHeaders i użyć małej funkcji pomocniczej do pobrania tokena nośnika:
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 "):]
Następnie następuje weryfikacja tego tokena w odniesieniu do emitenta i odbiorcy, których oczekuje brama, przy użyciu opublikowanych kluczy podpisywania dostawcy tożsamości (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.
Walidacja musi odbywać się wewnątrz procesu. Wykorzystanie zewnętrznego proxy do walidacji JWT może być skuteczne w topologiach o ograniczonym poziomie zaufania, ale pozbawia bramkę możliwości narzucenia mapowania zakresu na narzędzie przed uruchomieniem efektów ubocznych tego narzędzia.
Wysyłanie: w imieniu serwera wymiany
Granica wysyłania polega na zamianie tokena użytkownika przychodzącego na token skierowany do odbiorców po stronie docelowej. To właśnie ta zamiana odróżnia ten model od prostego przekierowywania, ponieważ API po stronie docelowej nigdy nie widzi bezpośrednio tokena przychodzącego. Bramka wysyła żądanie do punktu końcowego tokenów dostawcy tożsamości za pomocą urn:ietf:params:oauth:grant-type:jwt-bearer (Entra) lub żądanie assume-role-with-web-identity (AWS), przekazując token przychodzący jako informację dotyczącą podmiotu. Dostawca zwraca nowy token dla API po stronie docelowej, którego atrybut sub nadal odnosi się do oryginalnego użytkownika.
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"]
Wymieniony token jest następnie dołączany do wyjściowego żądania 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()
Mapowanie zakresów i propagacja tożsamości
Funkcje narzędzi deklarują wymagane zakresy podczas rejestracji, a mechanizm kontrolny egzekwuje je przed uruchomieniem samego kodu narzędzia:
@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")
Ponieważ mapowanie zakresów jest deklaratywne, można je sprawdzić. Każdy, kto audytuje bramkę, otrzymuje wszystkie narzędzia oraz wymagane przez nie zakresy bezpośrednio z rejestru, bez konieczności czytania kodu narzędzi.
Granica przychodzących połączeń jest identyczna u wszystkich dostawców chmury. Różni się jedynie sposób łączenia wyjściowego, a obie poniższe implementacje mają tę samą ogólną strukturę.
W AWS: API Gateway, Lambda i Cognito
W AWS granicą przychodzącą może być dostosowany autoryzator Lambda w Amazon API Gateway. Sprawdza on token w odniesieniu do puli użytkowników Amazon Cognito i zwraca dokument polityki wraz z mapą kontekstu zanalizowanych roszczeń, dzięki czemu Lambda serwera MCP może pobrać te roszczenia z event.requestContext.authorizer zamiast sprawdzać je ponownie. Jeśli chodzi o stronę wychodzącą, sts:AssumeRoleWithWebIdentity bierze token przychodzący (lub ten wydany przez pulę tożsamości Cognito) i generuje tymczasowe uprawnienia AWS powiązane z zintegrowaną tożsamością użytkownika. Każdy punkt końcowy API Gateway znajdujący się za nim lub zewnętrzna API, która ufa tokenom JWT Cognito, widzi wtedy użytkownika w roszczeniu sub.
Minimalna wymiana danych między Cognito a STS w funkcji Lambda API Gateway wygląda następująco:
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"]
Polityka zaufania przypisywanej roli określa zbiór użytkowników Cognito jako zasadniczą jednostkę federacyjną i sprawdza również wartość aud przychodzącego tokena, co oznacza, że tylko token wydany dla tego bramy może przyjąć tę rolę.
W Azure: proces Entra on-behalf-of
W Azure granica przychodzących danych sprawdza przychodzący token w odniesieniu do rejestracji aplikacji Entra bramy, używając URI identyfikatora aplikacji bramy jako grupy docelowej. W przypadku wysyłania danych uruchamiany jest proces wymiany typu Entra on-behalf-of skierowany do rejestracji aplikacji samej API docelowej:
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 docelowe musi akceptować tokeny, których odbiorcą jest jego własna rejestracja aplikacji Entra. Jego logika autoryzacji odczytuje wartość oid (identyfikator obiektu) lub upn (nazwa głównego użytkownika), aby przypisać wywołanie oryginalnemu użytkownikowi. Struktura wzorca jest identyczna, zmienia się jedynie sposób połączenia z dostawcą tożsamości.
Łączenie z logowaniem i oceną
Propagowanie tożsamości przynosi największe korzyści, gdy jest ono rejestrowane. Jeśli brama loguje użytkownika przy każdym wywołaniu narzędzia, audyt może powiązać działanie agenta z konkretną osobą. To samo pole może być uwzględniane w rekordach oceny, łącząc wyniki testów z osobami, których zachowanie jest przedmiotem reakcji systemu. Traktuj tożsamość wyodrębnioną przez ten wzorzec jako pierwszą kolumnę każdego rekordu audytu i oceny generowanego przez system.
Kiedy ten wzorzec pasuje, a kiedy nie
Pasuje wtedy, gdy:
- Brama dostępu obsługuje systemy, w których indywidualna tożsamość ma znaczenie dla autoryzacji, audytu lub zgodności z regulacjami, takie jak zarządzanie usługami IT, dział kadr, finanse oraz zregulowane procesy pracy.
- Dostawca tożsamości przedsiębiorstwa wspiera standardy wymiany tokenów: Entra OBO, Cognito z tożsamością internetową STS lub wymiana tokenów w Auth0 i Okta.
- API znajdujące się poniżej w łańcuchu przepływu danych znajdują się w tym samym obszarze zaufania co brama dostępu i mogą być skonfigurowane tak, aby przyjmować tokeny użytkowników.
- Rozwój tego rozwiązania podlega standardom NIST AI RMF, SOX, HIPAA lub podobnym regulacjom, które wymagają odpowiedzialności na poziomie użytkownika za automatyczne działania.
Nie nadaje się w przypadku:
- Gdy każda wywołanie poniżej w łańcuchu przepływu danych wyraźnie nie jest specyficzne dla danego użytkownika, na przykład dane referencyjne dostępne tylko do odczytu bez żadnych efektów ubocznych związanych z konkretnym użytkownikiem.
- Zaufany proxy w stronie docelowej już implementuje pełną semantykę delegowaną dla bramy, co sprawia, że praca samej bramy staje się zbędna. W tym przypadku brama tworzy jedynie granicę przychodzących danych oraz wariant delegowanej zaufania dla strony wychodzącej, przekazując dane bezpośrednio na podstawie wyraźnej umowy zaufania i akceptując, że to proxy stanowi rzeczywistą granicę bezpieczeństwa.
- API w stronie docelowej odrzuca tokeny użytkowników, a ich modyfikacja nie jest rozważana. Należy odłożyć to rozwiązanie do czasu, aż sytuacja się zmieni. Ucieczka się do konta usługowego „na razie” powinna być świadomą decyzją z zapisanym ryzykiem, a nie standardowym rozwiązaniem.
Kwestie operacyjne:
- Opóźnienie. Każda wymiana w OBO generuje dodatkową wizytę w punkcie końcowym tokenów dla każdej wywołanej funkcji. Zachowuj wymienione tokeny tak długo, jak długo ważny jest token przychodzący, oznaczony kluczem
(sub, downstream_audience), i nigdy nie przechowuj ich pomiędzy użytkownikami. - Regulacja dostępu dostawcy tożsamości. Limity dla punktów końcowych tokenów nie są proste. Zabezpiecz zapas mocy obliczeniowej i śledź wskaźniki niepowodzeń wymian.
- Odnowa. Długie sesje agentów trwają dłużej niż czas życia pojedynczego tokena. Zaplanuj rotację tokenów odnowy lub zaakceptuj fakt, że pętla agenta kończy się po wygaśnięciu jego tokena, co zazwyczaj jest właściwą strategią bezpieczeństwa.
- Plan awaryjny na konto usługowe. Zachowaj ścieżkę wymiany dla konta usługowego do zadań naprawdę bezużytkownikowych, takich jak sprawdzanie stanu lub planowane synchronizacje w tle. Ustal jasne zasady, ogranicz jej zakres i rejestruj wszystkie operacje.
Gdzie ten model zawodzi
Przypuszcza on, że dostawca tożsamości obsługuje standard wymiany informacji w imieniu innych, ale wiele starszych rozwiązań tego nie robi. W takich środowiskach strona wysyłająca musi korzystać z alternatywy polegającej na bezpośrednim przekazywaniu tokenów, przy czym każda taka transakcja musi być objęta pisemną umową o zaufaniu z każdą usługą docelową. Jest to akceptowalne w przypadku backendów znajdujących się w granicach zaufania bramy, ale niewystarczające, gdy usługę docelową prowadzi inny zespół lub dostawca zewnętrzny. Prawidłową drogą migracji jest aktualizacja dostawcy tożsamości, a nie trwałe poleganie na przekierowywaniu danych.
Przypuszcza również, że usługi poniżej w łańcuchu przetwarzania akceptują tokeny identyfikujące użytkowników i odbiorców. Wiele systemów korporacyjnych powstało przed wprowadzeniem upoważniania delegowanego i autoryzuje się jedynie na podstawie identyfikatorów usług. W takim przypadku należy poczekać, aż usługa zostanie zaktualizowana lub przed nią zostanie umieszczony proxy rozpoznający tożsamość. Częściowe wdrażanie tego rozwiązania, przy którym niektóre wywołania są przypisywane użytkownikom, a inne usługom, jest normalnym stanem podczas migracji, ale powinno być traktowane jako dług techniczny z określoną datą zamknięcia, a nie jako standardowa praktyka.
Wreszcie nie rozwiązuje ono problemu odpowiedzialności agenta jako podmiotu działającego. Zachowanie tożsamości człowieka podczas przekierowania przez bramkę jest właściwą podstawową kontrolą, jednak agent LLM może nadal zostać skłoniony do podjęcia działań, których użytkownik nigdy dosłownie nie zażądał. Ten wzorzec gwarantuje przypisanie odpowiedzialności, a nie intencji. Kontrole kompensacyjne niosą tę odpowiedzialność: potwierdzenie przez człowieka w przypadku działań o dużym wpływie, minimalny zakres działania na granicy przychodzących zapytań oraz rejestracja dowodów dla każdej rozmowy.
Lista kontrolna wdrożenia
- Zdokumentuj umowę przychodzącą (wydawca, odbiorcy, obsługiwane zakresy) jako interfejs w wersjach i zarządzaj nią jak publiczną API.
- Zaimplementuj mapowanie zakresów na narzędzia jako deklaratywne pole podczas rejestracji narzędzia, a nie jako ad hoc sprawdzenia rozproszone w treści narzędzia.
- Ustaw wymianę w imieniu innych z dostawcą tożsamości przedsiębiorstwa dla każdego backendu poniżej w łańcuchu. Uczyń OBO standardowym rozwiązaniem i zachowaj bezpośrednie przekierowanie dla backendów znajdujących się wewnątrz wyraźnych granic zaufania bramy.
- Zachowuj w pamięci tymczasowej wymienione tokeny przez cały czas życia tokena przychodzącego, klasyfikując je według użytkownika oraz grupy docelowej, i nigdy nie udostępniaj ich między użytkownikami.
- Przy każdej wywołanej funkcji zapisuj zarówno wartość
subprzychodzącą, jak i wychodzącą, aby można było prześledzić cały proces propagacji od początku do końca. - Zezwalaj na jeden ściśle określony ścieżkę konta usługowego do pracy bez udziału użytkownika i wymagaj zatwierdzenia przy każdym jego użyciu.
- Zapisuj API poniżej w łańcuchu, które odrzucają tokeny przypisane do konkretnych użytkowników, jako element architektonicznego długu i pracuj nad usunięciem tej ograniczenia.
Gdy amerykańskie przedsiębiorstwa i programy federalne przyjmują Ramy zarządzania ryzykiem sztucznej inteligencji NIST, ich funkcja Govern wymaga, aby indywidualna odpowiedzialność przetrwała automatyczne działania na systemach regulowanych. Zakończenie procesu identyfikacji przy bramce dostępu jest wprost sprzeczne z tym wymogiem i może prowadzić do stwierdzeń z audytów w środowiskach SOX, HIPAA i FedRAMP, gdzie każda automatyczna zmiana stanu musi być powiązana z konkretną osobą.
Dalsza lektura
Podstawami są RFC 6749 (Ramy autoryzacji OAuth 2.0) oraz RFC 8693 (Wymiana tokenów w OAuth 2.0) opracowane przez IETF. Dokumentacja dostawców omawia mechanizm OAuth 2.0 On-Behalf-Of w platformie identyfikacyjnej Microsoft, referencję API AWS STS AssumeRoleWithWebIdentity oraz zbiory tożsamości Amazon Cognito. NIST publikuje Ramy zarządzania ryzykiem sztucznej inteligencji (AI RMF 1.0), a specyfikacja MCP zawiera rozdział dotyczący autoryzacji, który warto przeczytać razem ze wszystkimi powyższymi materiałami.