Łączysz Claude Code z serwerem MCP. Serwer wymaga uwierzytelnienia. Twój klient wysyła żądanie; serwer odpowiada nagłówkiem 401 Unauthorized i WWW-Authenticate: Bearer, który zawiera URL resource_metadata. Co dzieje się następnie, to przepływ OAuth 2.1 zdefiniowany przez trzy RFC i zestaw rozszerzeń specyficznych dla MCP, które razem rozwiązują jeden z trudniejszych problemów w bezpieczeństwie agentów: upewnienie się, że token uzyskany do komunikacji z tym serwerem MCP nie może być ponownie użyty wobec tamtego.
Ten wpis śledzi cały przepływ — co mówi specyfikacja, co RFC faktycznie zapewniają i gdzie w praktyce kryją się pułapki bezpieczeństwa.
Przepływ: od 401 do tokena powiązanego z zasobem
Model autoryzacji MCP (wersja 2025-11-25, przeniesiona bez zmian do wersji kandydującej — zamrożona 2026-05-21 — dla ostatecznej specyfikacji zaplanowanej na 2026-07-28) jest procesem dwufazowym. Faza 1 to wykrywanie: 401 przenoszący WWW-Authenticate: Bearer resource_metadata="..." informuje klienta, że ten serwer jest chroniony przez OAuth i gdzie znaleźć jego metadane. Faza 2 to dostęp autoryzowany: wykorzystać metadane, odkryć serwer autoryzacyjny, uzyskać token powiązany z tym konkretnym serwerem i go użyć.
Konkretne kroki:
-
401 + WWW-Authenticate: Serwer MCP odrzuca nieautoryzowane żądanie. Parametr
resource_metadataw wyzwaniu wskazuje dokument Metadanych Zasobu Chronionego serwera. -
PRM pobieranie (RFC 9728): Klient wykonuje żądanie GET na URL
/.well-known/oauth-protected-resource. Odpowiedź to dokument JSON deklarujący kanoniczny URI zasobu serwera, serwery autoryzacyjne, które go chronią, oraz zakresy obsługiwane przez zasób. Klient weryfikuje, że poleresourcew dokumencie odpowiada serwerowi, do którego zamierzał się połączyć — niezgodność jest sygnałem podszywania się i musi zostać odrzucona. -
AS odkrywanie (RFC 8414): Korzystając z URL wystawcy z tablicy
authorization_serversz PRM, klient przechodzi przez dobrze znane kandydatury (najpierw/.well-known/oauth-authorization-server, potem/.well-known/openid-configuration), aby pobrać metadane serwera autoryzacji. Wystawca w zwróconym dokumencie musi być identyczny Bajtowo (byte-identyczny) z wystawcą, dla którego został pobrany — nie „równoważny po normalizacji”, nie „wystarczająco bliski”. Identyczny bajtowo. -
Pozyskiwanie tokenu (RFC 8707): Klient wysyła żądanie tokenu do punktu końcowego tokenu AS, uwzględniając
resource=<canonical server URI>zarówno w żądaniach autoryzacyjnych, jak i w żądaniach tokenu. To wiąże odbiorcę tokenu z tym konkretnym serwerem MCP. AS wydaje token, którego odbiorcą jest ten zasób, a klient przedstawia go serwerowi. -
Autoryzowany dostęp: Klient używa tokenu do wywoływania metod introspekcji tylko do odczytu serwera MCP (
tools/list,resources/listitp.). Token potwierdza, że klient jest autoryzowany; wskaźnik zasobu potwierdza, że token został wygenerowany dla tego serwera.
Co faktycznie zapewnia RFC 9728
RFC 9728 (Metadane chronionego zasobu) jest mechanizmem odkrywania. Odpowiada na pytanie: „który serwer autoryzacyjny chroni ten zasób i czego oczekuje?” Nie uwierzytelnia serwera, nie weryfikuje tokenu ani nie egzekwuje kontroli dostępu. Są to odrębne kwestie.
Dokument PRM ma jedno wymagane pole (resource) oraz zestaw opcjonalnych, z których najważniejsze jest authorization_servers. Specyfikacja MCP zaostrza opcjonalność RFC: wymagany jest co najmniej jeden serwer autoryzacyjny. Dokument PRM, który nie wymienia żadnego, traktowany jest jako błąd.
Pole resource jest kanonicznym URI chronionego zasobu. Klient musi porównać je z serwerem, z którym zamierzał się skontaktować, i odrzucić niezgodność:
// RFC 9728 §3.3: wartość zasobu PRM MUSI identyfikować chronione
// zasób, do którego odnosi się klient — proste porównanie ciągów znaków z
// wskaźnik zasobów, do którego ten klient przypisuje swoje tokeny.
if prm.Resource != c.resource {
return authServerMetadata{}, fmt.Errorf(
"mcp: oauth: protected resource metadata declares resource %q, "+
"expected %q (RFC 9728 §3.3 reject)", prm.Resource, c.resource)
}
Ta kontrola zapobiega klasie ataków, w których złośliwy lub błędnie skonfigurowany dokument PRM twierdzi, że reprezentuje inny zasób. Porównanie nie jest normalizowane — jest to bezpośrednie dopasowanie ciągu znaków do kanonicznego URI zasobu, który klient obliczył dla podanego mu adresu URL serwera.
Wskaźniki zasobów RFC 8707 — dlaczego powiązanie tokena ma znaczenie
Bez wskaźników zasobów, token dostępu uzyskany z AS może potencjalnie być użyty na dowolnym serwerze zasobów chronionym przez AS. Jeśli masz dwa serwery MCP — na przykład serwer dokumentacji tylko do odczytu i piaskownicę do wykonywania kodu — oba za tym samym dostawcą tożsamości, token uzyskany dla jednego mógłby zostać przedstawiony drugiemu. To jest problem z tzw. „confused deputy”.
RFC 8707 rozwiązuje to, dodając parametr resource do żądań autoryzacji i tokenów. AS wydaje token, którego odbiorcą jest wyraźnie ten URI zasobu. Poprawnie walidujący serwer zasobów odrzuca token, którego odbiorca nie odpowiada jego własnej tożsamości.
W praktyce żądanie tokena zawiera wskaźnik zasobu wraz z przyznaniem:
form := url.Values{
"grant_type": {"client_credentials"},
"resource": {c.resource}, // RFC 8707 — powiąż token z odbiorcą
}
To pojawia się w każdym typie przyznawania, którego używa konektor: poświadczenia klienta, realizacja kodu autoryzacyjnego i rotacja tokena odświeżającego. Wskaźnik zasobu nie jest opcjonalny — jest obecny w każdym żądaniu tokena, więc każdy token jest z definicji powiązany z audytorium.
Sprawdzenie identyczności bajtowej wydawcy
Najbardziej krytyczna dla bezpieczeństwa weryfikacja w całym przepływie jest jednocześnie najprostsza do sformułowania i najłatwiejsza do popełnienia błędu: wartość wydawcy w dokumencie metadanych AS musi być identyczna bajt po bajcie z wydawcą, którego klient użył do skonstruowania dobrze znanego URL.
Nie równa się bez uwzględnienia wielkości liter. Nie jest równoważna po normalizacji schematu. Nie jest taka sama po usunięciu końcowego ukośnika. Identyczna bajt po bajcie. Sekcja 3.3 RFC 8414 jasno to określa, a specyfikacja MCP przejmuje to wymaganie.
// discoverASMetadata: ODRZUCA dokument, którego issuer nie jest
// BAJTOWO IDENTYCZNY z issuer, dla którego go pobrano (RFC 8414 §3.3).
// Niezgodny dokument jest sygnałem podszywania się.
if as.Issuer != issuer {
return authServerMetadata{}, fmt.Errorf(
"mcp: oauth: AS metadata at %s declares issuer %q, "+
"expected %q (RFC 8414 §3.3 reject)", cand, as.Issuer, issuer)
}
Dlaczego tak surowo? Ponieważ atakujący, który kontroluje DNS lub znajduje się na ścieżce sieciowej, może dostarczyć dokument metadanych wskazujący własny punkt końcowy tokenu, twierdząc, że jest prawowitym wystawcą. Jeśli klient znormalizowałby wystawcę przed porównaniem, https://auth.example.com i https://AUTH.example.com by się zgadzały — a dokument atakującego zostałby zaakceptowany. Porównanie identyczne bajtowo eliminuje ten problem.
Ta sama dyscyplina ma zastosowanie w RFC 9207 (walidacja wystawcy odpowiedzi autoryzacyjnej). Gdy klient rozpoczyna przepływ kodu autoryzacyjnego, zapisuje wystawcę z zweryfikowanych metadanych AS. Przy przekierowaniu z powrotem parametr iss w odpowiedzi musi odpowiadać tej zapisanej wartości — ponownie, bajt po bajcie, bez normalizacji — przed zrealizowaniem kodu autoryzacyjnego. To jest obrona przed pomyłką: bez tego, złośliwy AS mógłby przechwycić kod i sprawić, że klient zrealizuje go w punkcie końcowym tokenów atakującego.
Identyfikacja klienta: CIMD zastępuje DCR
Specyfikacja MCP definiuje kolejność priorytetową, według której klient identyfikuje się wobec serwera autoryzacyjnego:
- Wstępnie zarejestrowane poświadczenia — operator przygotowuje
client_idiclient_secretz wyprzedzeniem - CIMD (Dokumenty metadanych identyfikatora klienta) — klient hostuje dokument JSON pod adresem URL HTTPS; ten adres URL jest
client_id - Dynamiczna rejestracja klienta (RFC 7591) — klient rejestruje się na punkcie końcowym rejestracji AS
- Wywołanie użytkownika — nie dotyczy agentów bez interfejsu użytkownika
DCR jest przestarzałe w wersji kandydującej do finalnej specyfikacji zaplanowanej na 2026-07-28, na korzyść CIMD. Powód jest operacyjny: DCR tworzy trwały stan klienta na serwerze autoryzacji. Każdy agent, który się rejestruje, pozostawia parę client_id/client_secret, którą musi przechowywać AS, a nikt ich nie śledzi ani nie cofa. Dla floty agentów prowadzi to do niekontrolowanego rozprzestrzeniania poświadczeń.
CIMD odwraca model. Klient przechowuje dokument pod adresem URL, którym zarządza. AS pobiera dokument, gdy potrzebuje zweryfikować klienta, buforuje go zgodnie z nagłówkami buforowania HTTP i niczego nie przechowuje na stałe. Rotacja kluczy to aktualizacja dokumentu. Wycofanie klienta to usunięcie dokumentu. Żadne osierocone rejestracje nie kumulują się na AS.
Kompromis: CIMD wymaga, aby klient uruchamiał punkt końcowy HTTPS. Dla samodzielnie hostowanej płaszczyzny zarządzania jest to naturalne — płaszczyzna już uruchamia usługi HTTPS. Dla narzędzia CLI na laptopie dewelopera jest to mniej naturalne, dlatego wstępnie zarejestrowane poświadczenia pozostają pierwszą opcją w kolejności priorytetów.
Tożsamość CIMD nie może przenosić wspólnego sekretu (adres URL dokumentu jest publiczny — sekret w nim byłby wyciekiem poświadczeń). Uwierzytelnianie klienta używa private_key_jwt (RFC 7523): klient podpisuje krótkotrwały JWT prywatnym kluczem, którego publiczny odpowiednik jest opublikowany w polu jwks dokumentu CIMD.
Reguła „nigdy nie przekazuj dalej”
Strukturalna obrona, którą łatwo przeoczyć: łącznik używa tylko tokena, który sam uzyskał, dla konkretnego serwera, poprzez opisywany powyżej proces odkrywania. Nigdy nie akceptuje tokena od osoby trzeciej ani nie przekazuje go do serwera MCP. Nigdy nie odczytuje tokena z przychodzącego żądania i nie przesyła go dalej.
To jest obrona przed zdezorientowanym pełnomocnikiem na poziomie protokołu. Gdyby klient przekazywał tokeny, które otrzymał, napastnik mógłby przedstawić token przypisany do zasobu o niskich uprawnieniach i sprawić, że klient przekaże go do zasobu o wysokich uprawnieniach (lub odwrotnie — uzyskać token o wysokich uprawnieniach, oszukując klienta, aby przedstawił go do serwera kontrolowanego przez napastnika). Poprzez uzyskiwanie własnych tokenów i nigdy nie używanie cudzych, łącznik nie może zostać zmuszony do działania jako przekaźnik tokenów.
Przekazywanie tokenów nie jest tylko odradzane — jest strukturalnie niemożliwe. Klient HTTP łącznika dla przepływów OAuth jest oddzielny od jakiegokolwiek obsługiwacza żądań przychodzących. Nie ma ścieżki kodu, która odczytuje token dostępowy (bearer token) z przychodzącego żądania i zapisuje go w wychodzącym.
SSRF: ochrona przed ponownym wiązaniem DNS
Każdy URL punktu końcowego metadanych i tokenów, który łącznik pobiera, jest chroniony przed SSRF na dwóch warstwach. Pierwsza to kontrola wstępna: URL musi być HTTPS (poza loopbackiem dla lokalnego rozwoju) i nie może być dosłownym zarezerwowanym adresem IP.
Drugim jest kontrola w czasie wybierania, która eliminuje lukę TOCTOU w przypadku DNS-rebinding. Nazwa hosta, która rozwiązuje się do publicznego adresu IP podczas wstępnej kontroli, może przy ponownym wywołaniu gniazda zostać powiązana z prywatnym adresem IP. Klient HTTP konektora instaluje funkcję net.Dialer.Control, która sprawdza konkretny rozpoznany adres IP w momencie łączenia i odrzuca wszelki adres zastrzeżony. To jest weryfikacja autorytatywna — kontrola wstępna jest szybkim odrzutem w oczywistych przypadkach, ale kontrola w czasie wybierania jest tą, która ma znaczenie.
Podwyższenie zakresu
Serwer MCP może odpowiedzieć na żądanie kodem WWW-Authenticate: Bearer error="insufficient_scope" scope="mcp:tools:list mcp:resources:read". Specyfikacja (SEP-835/SEP-2350) definiuje, jak klient powinien to obsługiwać: należy obliczyć sumę zbioru zakresów, które wcześniej żądał klient, oraz zakresów, które serwer właśnie zakwestionował, a następnie ponownie uzyskać token z tym rozszerzonym zestawem. Suma zachowuje wcześniej przyznane uprawnienia i dodaje nowe. Podwyższenie poziomu odbywa się jednokrotnie — drugie zgłoszenie niedostatecznego zakresu w tym samym żądaniu nie jest ponawiane, aby zapobiec nieskończonym pętlom.
Serwer może być bezstanowy w swoim wyzwaniu: podaje tylko zakresy potrzebne do aktualnej operacji, a nie pełny zestaw, o który klient mógł wcześniej poprosić. Akumulacja po stronie klienta umożliwia to działanie bez konieczności śledzenia historii zakresów dla każdego klienta przez serwer.
Co to oznacza w praktyce
Przepływ OAuth dla serwerów MCP jest dobrze określony i, gdy jest ściśle wdrożony, rozwiązuje rzeczywiste powierzchnie ataków: powtórne użycie tokenów między serwerami, podszywanie się pod metadane, SSRF z ponownym wiązaniem DNS oraz niekontrolowane rozprzestrzenianie poświadczeń klienta. Sprawdzenie identyczności bajtów wydawcy, wskaźnik zasobów w każdym żądaniu tokena oraz strukturalna zasada braku przejścia są podstawową ochroną. Nie są to funkcje, które można wdrożyć częściowo — każda z nich jest twardym sprawdzeniem, które kończy się niepowodzeniem w przypadku błędu.
Trudniejszym problemem jest sprawa operacyjna. Zespoły wdrażające serwery MCP muszą:
- Opublikować ważny dokument PRM w
/.well-known/oauth-protected-resourcez polemresourceodpowiadającym kanonicznemu URI ich serwera - Użyj AS, który obsługuje wskaźniki zasobów — wielu dostawców tożsamości nadal tego nie robi lub traktuje parametr
resourcejako doradczy, a nie powiązany z odbiorcą - Przejdź z DCR na CIMD zanim wycofanie zostanie zamienione na usunięcie, lub zarejestruj poświadczenia wprost
- Przypisz poświadczenia do wydawcy aby zapobiec cichym powtórnym użyciom między wydawcami, gdy zmieni się topologia AS
Dokumentacja MCP łącznika opisuje konfigurację operacyjną, a model bezpieczeństwa wyjaśnia, jak token powiązany z zasobem wpasowuje się w szerszą mapę dostępu.