Jan 5, 2026
13 min

OAuth 2.1 dla Model Context Protocol (MCP) z AWS Cognito

OAuth dla MCP z AWS Cognito

Jeśli wdrażasz zdalny serwer MCP (Streamable HTTP) i chcesz udostępnić go użytkownikom ChatGPT/Claude/VS Code, musisz mieć poprawnie zaimplementowane uwierzytelnianie. W tym artykule przechodzimy przez MCP OAuth 2.1, wyjaśniamy problem z DCR (RFC 7591) w AWS Cognito i pokazujemy sprawdzony workaround: stały client_id + własne endpointy /.well-known oraz proxy /register i /token, z pełnym wsparciem dla PKCE (S256) i weryfikacją JWT.

TL;DR

MCP OAuth z AWS Cognito w 5 krokach

  • konfiguracja AWS Cognito
  • wystawiamy /.well-known/oauth-protected-resource/mcp
  • wystawiamy /.well-known/oauth-authorization-server
  • robimy proxy /register + opcjonalnie /token
  • weryfikujemy JWT na endpointach MCP

Jak działa uwierzytelnianie w Model Context Protocol (MCP OAuth 2.1)?

Proces uwierzytelniania w MCP jest dobrze opisany i prosty, o ile jest dokładnie implementowany. Cały proces opiera się na 5 dokumentach RFC:

Proces OAuth w MCP

  1. Klient MCP wykonuje zapytanie bez tokena.
  2. Serwer odpowiada kodem 401 i nagłówkiem WWW-Authenticate, w którym podaje adres Protected Resource Metadata:
1Bearer resource_metadata=https://example.com/.well-known/oauth-protected-resource/mcp
2
  1. Klient MCP wykonuje zapytanie GET pod ten adres i otrzymuje szczegóły:
    1. resource - istotne przy kolejnych etapach uwierzytelniania.
    2. authorization_servers - informacja, z kim dalej rozmawiać.
JSON
1{
2"resource": "https://resource.example.com",
3"authorization_servers":
4  ["https://as1.example.com"]
5}
6
  1. Klient MCP wykonuje zapytanie do authorization_servers po Authorization Server Metadata. Jest to większy dokument JSON, w którym znajdują się informacje o tym, co wspiera serwer uwierzytelniający, jakie są endpointy, scope itd.
  2. Następnie następuje komunikacja z serwerem uwierzytelniającym:
    1. Client register - tu zwracany jest Client Id, Client Secret.
    2. Authorize - tu prosimy użytkownika o zalogowanie/rejestrację.
    3. Token exchange - wymieniamy token z logowania na Bearer token.
  3. Klient MCP z Bearer token może komunikować się bezpiecznie z serwerem MCP.

Co to jest DCR (Dynamic Client Registration)?

Dynamic Client Registration jest protokołem opisanym w RFC 7591. Jest to mechanizm umożliwiający dynamiczne tworzenie klientów OAuth 2.0 na serwerze autoryzacyjnym. W praktyce działa to tak, że aplikacja komunikuje się z serwerem autoryzacyjnym i prosi o utworzenie nowego klienta oraz zwrócenie ClientId i ClientSecret. Dzięki temu aplikacja nie musi wcześniej znać tych wartości, a serwer autoryzacyjny generuje unikalne wartości. Problemem jest to, że serwer autoryzacyjny nie ma jak sprawdzić, czy dana aplikacja ma już wygenerowanego klienta, i generuje nowego. Nowy klient OAuth jest tworzony za każdym razem gdy użytkownik dodaje serwer MCP do ChatGPT/Claude/inne. Powoduje to bałagan i utrudnia zarządzanie:

<ZDJĘCIE>

Rozwiązaniem tego jest skorzystanie z Client ID Metadata Documents, które pozwala rozpoznać klientów i zwrócić wcześniej utworzone klucze. 25.11.2025 w nowej wersji standardu MCP Client ID Metadata Documents będzie wspierany, ale musi być również zaimplementowany przez klienta MCP oraz serwer autoryzacyjny. Dlatego DCR albo zmodyfikowana wersja będzie jeszcze obowiązywała przez dłuższy czas.

Można też trochę oszukać i zwrócić wcześniej zdefiniowaną parę ClientId Client Secret. Dzięki temu:

  • unikamy problemów z dużą ilością klientów OAuth,
  • mamy kontrolę nad ClientId i ClientSecret,
  • kontrolujemy, jakie aplikacje mogą się połączyć (przez redirect_uri).

Ryzyka i konsekwencje:

  • nie jest to czysta implementacja specyfikacji,
  • część komunikacji przechodzi przez nasz serwer co może byc wąskim gardłem,
  • musimy dodawać redirect_uri za każdym razem, gdy chcemy wspierać nowego klienta MCP,
  • trzeba zadbać o odpowiednie logowanie/audyt danych

Kiedy nie obchodzić DCR?

  • kiedy serwer uwierzytelniający wspiera DCR
  • chcesz być w pełni zgodny i nie chcesz by flow uwierzytelniania przechodziło przez twój serwer

Cognito

AWS Cognito to zarządzana usługa do uwierzytelniania i autoryzacji użytkowników oraz aplikacji. Oferuje gotowe OAuth 2.0 / OIDC, JWT, federację tożsamości (IdP zewnętrzne, SAML/OIDC), User Pools i Identity Pools, a także integrację z IAM. Pozwala szybko wdrożyć bezpieczny token-based access, kontrolę scope’ów/claims oraz skalowalne zarządzanie użytkownikami bez utrzymywania własnej infrastruktury. Jest to odpowiednik Auth0, gdy chcesz korzystać z chmury AWS.

Cognito nie wspiera out-of-the-box DCR. By użyć Cognito, musimy dopisać trochę własnego kodu.

MCP + OAuth z Cognito w kodzie

Główna różnica między standardowym DCR a podejściem, które tu jest opisane, to zmodyfikowanie procesu rejestracji. Zamiast tworzyć nowy App Clients dla User Pool, zwracamy wcześniej utworzony. Dzięki temu unikamy bałaganu związanego z dużą ilością App Clients, ale musimy sami zarządzać Allowed callback URLs, by dopuścić klientów MCP do naszego uwierzytelniania.

Przykłady są robione w bibliotece Hono.js.

Przygotowanie w Cognito

Co musisz przygotować:

  • Traditional web application,
  • ustawienie Allowed callback URLs,
  • Managed login style,
  • ustawienie Resource servers.

Większość pracy odbywa się w kodzie, ale to nie znaczy, że nie ma nic do zrobienia po stronie AWS’a. Po pierwsze, trzeba utworzyć i skonfigurować User Pool oraz utworzyć Traditional web application (App Clients). To są standardowe elementy przy konfiguracji Cognito i nie będę tutaj tego omawiać. By zadziałało nam OAuth dla MCP, musimy zadbać jeszcze o parę rzeczy.

Ustawienie Allowed callback URLs

To też jest standard przy konfiguracji App Clients. W odróżnieniu od standardowych aplikacji będziemy mieć tu dużo więcej adresów URL (jeśli zawsze zwracamy jeden App Client). Poniżej masz listę URL, które ustawiałem podczas testowania. Pamiętaj, że to może się zmienić i w przypadku błędów warto sprawdzić oficjalną dokumentację od klienta MCP.

Allowed callback URI setting
Sprawdź czy masz dobrze ustawione Allowed Callback URIs

Lista callback URLs dla OAuth MCP:

  • lokalne testy:
    • http://localhost:6274/oauth/callback/debug
  • ChatGPT:
    • https://chatgpt.com/connector_platform_oauth_redirect
  • Claude:
    • https://claude.ai/api/mcp/auth_callback
    • https://claude.com/api/mcp/auth_callback
  • VSCode:
    • https://insiders.vscode.dev/redirect
    • https://vscode.dev/redirect

Managed login style

Kolejnym istotnym elementem jest włączenie Managed login (a nie poprzedniego Hosted UI). Tylko w Managed login jest możliwość przekazywania parametru resource, który jest istotny dla całego procesu. Jeśli tworzysz Cognito od zera, to Managed Login powinno być utworzone i mieć przypisany styl. Jeśli tworzysz kolejny App Client, to sprawdź, czy masz go przypisanego.

Manage login style setting
Sprawdź czy masz przypisany styl

Resource servers

Resource jest dodatkowym parametrem zabezpieczającym komunikację i jest wysyłany przez klientów MCP. Przez to musi być odpowiednio skonfigurowany w serwerze uwierzytelniającym, by nie rzucać błędami.

Najlepiej iść w Resource jako ​Canonical Server URI np.: https://example.com/mcp. Z doświadczenia polecam dodać /mcp na końcu, bo inaczej to się potem dziwnie zachowuje. W Cognito ustawiasz to w Domain > Resource servers:

  • Name może być dowolny,
  • Resource server identifier -> musi być adresem URL.

Nie musisz definiować dodatkowych scope, ale jest to opcja, którą możesz dodatkowo wykorzystać.

Resource servers settings
Dodaj odpowiedni resource server

Zabezpieczenie endpointów

By wszystko poprawnie działało dla endpointu MCP, musimy sprawdzać, czy użytkownik jest uwierzytelniony. Można do tego użyć middleware w Hono. Standardowo sprawdzamy, czy istnieje nagłówek Authorization i czy ma poprawną wartość. Zamiast implementować sprawdzenie samemu, warto skorzystać z biblioteki aws-jwt-verify.

1import { CognitoJwtVerifier } from "aws-jwt-verify";
2
3const verifier = CognitoJwtVerifier.create({
4  userPoolId,
5  tokenUse: "access",
6  clientId,
7});
8
9const jwtMiddleware = createMiddleware(async (c, next) => {
10  const header = c.req.header('Authorization');
11  if (!header) {
12    console.log('Unauthorized request');
13    c.res.headers.set('WWW-Authenticate', `Bearer resource_metadata="${c.var.domain}/.well-known/oauth-protected-resource/mcp"`);
14    return c.json({ error: 'Unauthorized' }, 401);
15  }
16
17  try {
18    const payload = await verifier.verify(
19      header.split(' ')[1]
20    );
21    
22    console.log("Token is valid. Payload:", payload);
23  } catch {
24    console.log("Token not valid!");
25    return c.json({ error: 'Unauthorized' }, 401);
26  }
27  await next();
28});
29

Protected Resource Metadata

W tym dokumencie JSON musimy zdefiniować resource i authorization_servers. Resource musi być zgodny z tym, co skonfigurowaliśmy w Cognito jako Resource server identifier (czyli adres URL serwera). Dodatkowo jako authorization_servers dajemy nie link do Cognito, ale do naszego serwera, który będzie obsługiwał część ruchu.

Trzeba wystawić endpoint GET na resource:

1app.get('/.well-known/oauth-protected-resource/mcp', async (c) => {
2  return c.json({
3    resource: `https://example.com/mcp`,
4    "authorization_servers": [
5      "https://example.com"
6    ],  
7 });
8});
9

Authorization Server Metadata

Skoro nasz serwer jest serwerem uwierzytelniającym, to musimy wystawić endpoint z Authorization Server Metadata.

1auth.get('/.well-known/oauth-authorization-server', async (c) => {
2
3});
4

I tu warto się zatrzymać na chwilę, bo jest parę rzeczy, o których warto wiedzieć.

Po pierwsze, Cognito udostępnia endpoint, pod którym można uzyskać konfigurację openid:

1https://cognito-idp.REGION.amazonaws.com/YOUR_USER_POOL_ID/.well-known/openid-configuration
2

Trzeba zamienić REGION i YOUR_USER_POOL_ID na odpowiednie wartości, by uzyskać JSON z konfiguracją.

Druga rzecz to brak code_challenge_methods_supported w tym endpoincie. W specyfikacji jest on opcjonalny, ale brak tego parametru wskazuje na brak wsparcia dla PKCE. A Cognito to jak najbardziej wspiera. Dodatkowo nie możemy tego pominąć, ponieważ specyfikacja MCP jest w tej kwestii konkretna.

MCP clients MUST verify the presence of code_challenge_methods_supported in the provider metadata response. If the field is absent, MCP clients MUST refuse to proceed.

Po trzecie, trzeba pamiętać o zmianie endpointu do rejestracji i tokenu.

Łącząc wszystko w całość, mamy:

1app.get('/.well-known/oauth-authorization-server', async (c) => {
2  const configuration = await fetch(`https://cognito-idp.${region}.amazonaws.com/${pool_id}/.well-known/openid-configuration`);
3  const configJson = await configuration.json();
4  return c.json({
5    ...configJson,
6    "registration_endpoint": new URL('/register', c.get('domain')).toString(),
7    "token_endpoint": new URL('/token', c.get('domain')).toString(),
8    "code_challenge_methods_supported": ["S256"],
9  });
10});
11

/register endpoint

Podmiana endpointu rejestracji pozwala nam zapanować nad procesem tworzenia klientów OAuth. Możemy tutaj tworzyć osobne App clients (i w ten sposób zaimplementować DCR) albo zawsze zwracać konkretne client_id. Wystarczy, że przygotujemy odpowiednią zwrotkę dla tego endpointu i wstawimy wartości. Co więcej, możemy wykorzystać to miejsce, by nie podawać client_secret i wstawić placeholder. Daje nam to przewagę, że klient MCP nigdy nie pozna client_secret, więc jest większe bezpieczeństwo. Ale wtedy zapytanie o token musi przechodzić przez nasz serwer, bo trzeba podmienić client_secret.

1auth.post('/register', async (c) => {
2  const req = await c.req.json();
3  return c.json({
4    redirect_uris: req.redirect_uris,
5    "client_id": client_id,
6    "scope": `openid email phone profile`,
7    "client_secret": "placeholder_secret",
8    grant_types: ['authorization_code', 'refresh_token'],
9    response_types: ['code'],
10  });
11});
12

/token endpoint

To jest potrzebne tylko, jeśli nie podałeś client_secret dla klienta MCP.

Ostatnia rzecz, którą trzeba zrobić, to obsłużyć endpoint /token, którego zadaniem jest wymiana tokenu z logowania na Bearer token do komunikacji z serwerem.

Nie jest to skomplikowane, bo jedyne, co trzeba zrobić, to dorzucić client secret. Najprościej to zrobić, dodając nagłówek Authorization z wartością Basic Base64Encode(client_id:client_secret).

1app.post('/token', async (c) => {
2  console.log('Token request received');
3  
4  const req = await c.req.text();
5
6  const params = new URLSearchParams(req);
7  const tokenData = Object.fromEntries(params.entries());
8
9  const url = `https://${userPoolId}.auth.eu-central-1.amazoncognito.com/oauth2/token`;
10
11  const response = await fetch(url, {
12    method: 'POST',
13    headers: {
14      'Content-Type': 'application/x-www-form-urlencoded',
15      'Authorization': `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString('base64')}`
16    },
17    body: new URLSearchParams({
18      ...tokenData
19    })
20  });
21  const responseBody = await response.json();
22
23  return c.json(responseBody);
24});
25

Inne rzeczy o których warto pamiętać

Z racji tego, że cześć komunikacji dotyczącej uwierzytelniania przechodzi przez nasz serwer warto dodatkowo zabezpieczyć. Kwestie szczegółów zabezpieczenia serwera wychodzi poza ten post ale pamiętaj o:

  • Rate limiting na /register i /.well-known/*
  • Odpowiednia konfiguracja CORS
  • Audit log: redirect_uri, client_name (jeśli jest), IP, user-agent
  • Cache dla openid-configuration (żeby nie fetchować w kółko)
  • Wymuszenie HTTPS

Testowanie OAuth w MCP z MCP Inspector

Oczywiście trzeba wszystko przetestować i mamy tutaj 2 opcje:

  • wykorzystanie klienta MCP, który ma wsparcie dla OAuth, np.: Claude Desktop czy ChatGPT. Plusem jest testowanie produkcyjne, ale w przypadku błędów ciężko się debugguje.
  • wykorzystanie narzędzia do testów MCP. Łatwe debugowanie, ale to, że działa w debuggerze, nie znaczy, że będzie w pełni działać w kliencie końcowym (w narzędziu testowym mogą być drobne różnice w implementacji standardu OAuth).

Ogólnie polecam zacząć od narzędzia do testów, by pozbyć się głównych błędów, a potem przejść do konkretnych klientów MCP i tam przetestować produkcyjnie. Osobiście polecam @modelcontextprotocol/inspector od twórców MCP. Można zainstalować przy pomocy npx i testować MCP zarówno STDIO, jak i Streamable HTTP.

1npx @modelcontextprotocol/inspector
2

Dla Auth jest osobna zakładka, gdzie można krok po kroku przejść proces uzyskiwania tokena.

Auth settings in mcp inspector
W zakładce Auth można krok po kroku sprawdzić konfiguracje

Dzięki temu można krok po kroku przejść przez cały proces i go sprawdzić.

Czytaj więcej

Jak stworzyć serwer MCP? STDIO + Streamable HTTP z Hono
Jan 5, 2026
Jak stworzyć serwer MCP? STDIO + Streamable HTTP z Hono
Tworząc własny serwer MCP możemy dostarczyć nasze dane do zewnętrznych aplikacji, które wspierają MCP. W artykule znajdziesz krok po kroku jak to zrobić.
Brama do wszystkich modeli AI. Jak uruchomić LiteLLM na AWS?
Jan 5, 2026
Brama do wszystkich modeli AI. Jak uruchomić LiteLLM na AWS?
Ilość modeli AI z jakimi możemy się integrować może przyprawić o ból głowy. Rozwiązaniem jest jednolite API z wykorzystaniem LiteLLM
MCP + AI SDK - jak to zintegrować i czy ma to sens?
Jan 5, 2026
Co to jest MCP? I jak zintegrować serwer MCP z AI SDK?
MCP jest równie gorącym tematem w bańce AI jak architektura agentowa. Ale czy warto się tym interesować? I jak można to wdrożyć z pomocą AI SDK od Vercel'a?