CodeGym /Kursy /ChatGPT Apps /Architektura autoryzacji MCP: MCP Client, MCP Server, MCP...

Architektura autoryzacji MCP: MCP Client, MCP Server, MCP Auth Server

ChatGPT Apps
Poziom 10 , Lekcja 1
Dostępny

1. O czym jest ten wykład i czego w nim nie ma

To będzie bardzo ciekawy wykład, w którym:

  • składamy w głowie obraz „trójkąta zaufania” pomiędzy MCP Client, MCP Server i MCP Auth Server — z użytkownikiem, który stoi „nad” tym trójkątem jako właściciel zasobów;
  • omawiamy flow: kto komu wysyła token, gdzie użytkownik się loguje i dlaczego serwer MCP nigdy nie widzi jego hasła;
  • łączymy to z naszym backendem Next.js/MCP i przyszłą konfiguracją Keycloak/Auth0.

Czego dziś nie robimy:

  • nie klikamy checkboxów w Keycloak i nie konfigurujemy konkretnego IdP;
  • nie piszemy pełnej weryfikacji JWT ani introspekcji — to tematy kolejnych wykładów (o Auth Server i o MCP Server jako chronionym zasobie).

Zadanie teraz — abyście mogli wziąć kartkę, narysować strzałki między ChatGPT, waszym serwerem i Auth0/Keycloak i bez zająknięcia wyjaśnić: gdzie jest login, gdzie token, gdzie dane.

2. Trójkąt zaufania: MCP Client, MCP Server, MCP Auth Server

Zacznijmy od ról. Techniczny „trójkąt zaufania” tworzą MCP Client, MCP Server i MCP Auth Server; użytkownik (User) — to osobna rola, właściciel zasobów, który stoi jakby nad tym trójkątem i wyraża zgodę na dostęp. W specyfice MCP i Apps SDK ta architektura jest dość jasno sformalizowana.

User (Resource Owner)

To człowiek po drugiej stronie ekranu. On/ona:

  • wchodzi do ChatGPT;
  • pisze prośbę „pokaż moje zamówienia / moje listy prezentów”;
  • wyraża zgodę na „powiązanie konta” waszej usługi z ChatGPT.

Najważniejsze: to on/ona jest właścicielem zasobów (historia zamówień, profile, listy prezentów) i to on/ona wyraża zgodę na dostęp do nich.

MCP Client

Tutaj jest to dla nas:

  • ChatGPT z Apps SDK;
  • czasem — MCP Jam Inspector (przy debugowaniu).

MCP Client potrafi:

  • czytać metadane waszego serwera MCP (przez .well-known);
  • uruchamiać OAuth‑flow w przeglądarce użytkownika;
  • przechowywać i dołączać tokeny do wywołań narzędzi MCP.

Warto pamiętać, że MCP Client to public client. Nie przechowuje waszego client_secret, więc rozmawia z Auth Serverem jak publiczne SPA: Authorization Code + PKCE.

MCP Server (Resource Server)

To wasz backend implementujący MCP:

  • nawiązuje połączenie z ChatGPT;
  • deklaruje narzędzia (tools), zasoby, prompty;
  • przy każdym wywołaniu narzędzia patrzy w nagłówek Authorization: Bearer <token>;
  • weryfikuje token (podpis, exp, aud, scope) i, jeśli wszystko jest OK, wykonuje logikę biznesową.

Kluczowy punkt: serwer MCP nie zajmuje się logowaniem. Nie widzi haseł, nie rysuje formularza logowania, nie wysyła użytkownikowi maila „potwierdź email”. Ufa wyłącznie kryptograficznie podpisanym tokenom od Auth Servera.

MCP Auth Server (Authorization Server / IdP)

To osobna usługa uwierzytelniania i autoryzacji: Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD itd.

Odpowiada za:

  • UI logowania (email/hasło, SSO, 2FA);
  • przechowywanie kont użytkowników;
  • wydawanie tokenów (access token, refresh token);
  • publikację metadanych OAuth/OIDC (/authorize, /token, jwks_uri, /registration itp.).

Dla MCP powinien wspierać OAuth 2.1 dla public clients (PKCE S256, dynamic client registration itp.).

Tabela podsumowująca role

Kto Co robi Czego nie robi
User Wprowadza login/hasło, wyraża zgodę na dostęp do danych Nie komunikuje się bezpośrednio z MCP Server
MCP Client (ChatGPT/Jam) Inicjuje OAuth, przechowuje token, wywołuje MCP tools Nie weryfikuje haseł, nie weryfikuje podpisu tokena
MCP Server Weryfikuje tokeny, wykonuje logikę biznesową tools Nie rysuje formularza logowania, nie przechowuje haseł
MCP Auth Server Loguje użytkownika, wydaje tokeny Nie zna waszych narzędzi MCP ani ich logiki biznesowej

Jeśli dotąd wszystko mieszało się w głowie w jeden „duży serwer, który robi wszystko” — pora to rozdzielić.

3. Jak wygląda flow: od „brak tokena” do chronionego wywołania narzędzi

Spójrzmy teraz na przepływ komunikatów. W specyfikacji MCP proces ten nazywa się „The Flow”: discovery → redirect → code → token → authorized calls.

Krok 0. Próba wywołania chronionego narzędzia bez tokena

Użytkownik pisze: „Pokaż moje zapisane pomysły na prezenty”.

ChatGPT jako MCP Client decyduje: „do tego trzeba wywołać narzędzie getUserGiftLists na naszym serwerze MCP”. Wykonuje wywołanie bez tokena (użytkownik jeszcze się nie logował).

Wasz serwer MCP:

  • widzi brak lub niepoprawny nagłówek Authorization;
  • odpowiada 401 Unauthorized i dodaje nagłówek WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" ze wskazaniem metadanych chronionego zasobu (resource metadata, o tym niżej).

To wygląda mniej więcej tak (logika, nie pełny HTTP):

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource"

ChatGPT widzi ten nagłówek i rozumie: „aha, zasób jest chroniony przez OAuth, trzeba wykonać OAuth‑flow i powiązać konto”.

Discovery: .well-known/oauth-protected-resource

Dalej MCP Client pobiera z waszego serwera metadane:

GET /.well-known/oauth-protected-resource

Serwer odpowiada dokumentem JSON z identyfikatorem zasobu i listą serwerów autoryzacji, u których należy uzyskiwać tokeny.

Minimalny przykład (szczegóły skonfigurujemy później, teraz ważna jest idea):

{
  "resource": "https://api.giftgenius.com",
  "authorization_servers": [
    "https://auth.giftgenius.com"
  ],
  "scopes_supported": ["gifts.read", "gifts.write"]
}

Gdzie:

  • resource — kanoniczny ID waszego zasobu; później powinien być użyty jako audience lub resource przy wydawaniu tokena;
  • authorization_servers — lista Auth Serverów, u których ChatGPT może poprosić o token;
  • scopes_supported — jakie „uprawnienia” wasz serwer MCP w ogóle rozumie.

Authorization Request: przekierowanie do Auth Server

Po otrzymaniu metadanych MCP Client idzie do Auth Server. Otwiera w przeglądarce kartę:

GET https://auth.giftgenius.com/authorize
    ?response_type=code
    &client_id=chatgpt-giftgenius
    &redirect_uri=... (adres zwrotny MCP Client)
    &code_challenge=...
    &code_challenge_method=S256
    &scope=openid gifts.read
    &resource=https://api.giftgenius.com

Użytkownik:

  • widzi znany ekran logowania (np. Keycloak lub Auth0);
  • wpisuje login/hasło, przechodzi 2FA;
  • potwierdza, że ChatGPT może czytać jego listy prezentów (scope gifts.read).

Code → Token: wymiana kodu na token z PKCE

Po udanym logowaniu Auth Server przekierowuje użytkownika z powrotem do MCP Client z code. MCP Client:

  • wykonuje POST na /token;
  • przekazuje code i code_verifier (odpowiadający code_challenge z poprzedniego kroku).

Auth Server weryfikuje PKCE: hashuje code_verifier, porównuje z pierwotnym code_challenge. Jeśli wszystko się zgadza i klient to rzeczywiście ten sam, który zaczął flow, to:

  • wydaje krótkotrwały access_token (zwykle JWT);
  • w nim wskazuje:
    • sub — ID użytkownika w Auth Server;
    • aud lub resource — wasz serwer MCP;
    • scope — dozwolone działania (gifts.read, openid itp.).

Authenticated Request: wywołanie narzędzia MCP z tokenem

Teraz MCP Client jest gotów ponownie wywołać wasze narzędzie, ale już z nagłówkiem:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Serwer MCP:

  • weryfikuje podpis tokena (na podstawie JWK Auth Server) lub przez introspekcję;
  • weryfikuje czas ważności (exp);
  • sprawdza aud / resource — czy token rzeczywiście został wydany dla https://api.giftgenius.com;
  • patrzy na scope i decyduje, czy można wywołać getUserGiftLists.

Po tym idzie już spokojnie do waszej bazy po jakiś userId i zwraca prywatne listy prezentów.

Zwróćcie uwagę, że do tego momentu mówiliśmy tylko o przepływie sieciowym: jak token jest pozyskiwany i dociera do serwera MCP. Dalej ważne jest zrozumieć, jak z sub i innych claims w tokenie powstaje konkretny userId w waszej bazie — tu do gry wchodzi identity bridge.

4. Identity Bridge: jak użytkownik ChatGPT zamienia się w userId w waszej bazie

Najciekawszy kawałek architektury — to „most tożsamości” (identity bridge). W specyfikacji MCP wyraźnie podkreślono: serwer MCP nie zna użytkowników ChatGPT, opiera się na danych w tokenie od Auth Server.

Schemat wygląda mniej więcej tak:

flowchart TD
  User[User w ChatGPT] -->|Login/SSO| Auth[Auth Server]
  Auth -->|JWT: sub, email, tenant| MCP[MCP Server]
  MCP -->|userId/tenantId| DB[(Wasza baza danych)]

Krok po kroku wygląda to tak.

Po pierwsze, Auth Server wewnątrz zna swoich użytkowników: ma encje user, email, id, być może tenant, roles. Przy udanym logowaniu umieszcza te informacje w tokenie (w claims):

{
  "sub": "auth0|abc123",
  "email": "user@example.com",
  "given_name": "Alice",
  "https://giftgenius.com/tenant": "tenant-42",
  "scope": "openid gifts.read",
  "aud": "https://api.giftgenius.com"
}

Po drugie, MCP Server podczas weryfikacji tokena wyciąga te claims i decyduje, kim jest użytkownik w jego świecie. Na przykład:

  • jeśli sub już istnieje w tabeli User.authProviderId — bierzemy powiązane userId;
  • jeśli nie — tworzymy lokalny rekord (on‑the‑fly provisioning) i wiążemy go.

Typowy fragment kodu TypeScript po stronie serwera MCP (uproczony, bez weryfikacji podpisu) może wyglądać tak:

type TokenClaims = {
  sub: string;
  email?: string;
  scope?: string;
};

async function mapClaimsToUserId(claims: TokenClaims): Promise<string> {
  const user = await db.user.findUnique({ where: { authSub: claims.sub } });
  if (user) return user.id;

  const created = await db.user.create({
    data: { authSub: claims.sub, email: claims.email ?? null }
  });
  return created.id;
}

Po trzecie, już po swoim userId serwer MCP pobiera wszystko, co trzeba: listy prezentów, historię zamówień, ustawienia, taryfę.

W ten sposób Auth Server staje się „mostem” między światem zewnętrznym (ChatGPT, Google, SSO) a waszym światem wewnętrznym (customer_id w bazie zamówień).

5. Dlaczego należy rozdzielać Auth Server i MCP Server

Może kusić: „A może mój serwer MCP sam wyświetli logowanie i sam wystawi token”. Formalnie to możliwe (możecie wbudować mini‑IdP), ale architektonicznie to zły pomysł. Powody są dość przyziemne.

Po pierwsze, bezpieczeństwo i skalowalność. Auth Server to ciężka maszyna: 2FA, social login, polityki haseł, blokady kont, odzyskiwanie dostępu, audyt logowań, być może certyfikacje. Pisanie tego od nowa w każdym mikroserwisie (w każdym serwerze MCP) — droga do piekła i PCI‑DSS. Znacznie prościej delegować to do Keycloak/Auth0 i tylko weryfikować ich token.

Po drugie, wymienność klienta. Dziś macie tylko ChatGPT. Jutro podłączycie Claude Desktop, własny frontend na Next.js, aplikację mobilną. Wszystkie one mogą używać tego samego Auth Server i tego samego schematu OAuth 2.1, a wasz serwer MCP po prostu nadal weryfikuje tokeny. Nie trzeba będzie przepisywać logiki biznesowej pod każdego nowego klienta.

Po trzecie, czystość kodu. Serwer MCP w idealnej wersji:

  • umie publikować /.well-known/oauth-protected-resource;
  • umie weryfikować token Bearer i wyciągać z niego userId, scopes, tenant;
  • implementuje narzędzia biznesowe (orders, gifts, profiles).

Cała logika UI logowania — formularze, layout, logowania społecznościowe — żyje w Auth Server i nie zaśmieca backendu.

6. Jak to wygląda w naszej aplikacji edukacyjnej GiftGenius

Wróćmy do aplikacji, którą prowadzimy przez kurs. Załóżmy, że mamy:

  • ChatGPT App „GiftGenius” z widgetem (Apps SDK), który potrafi dobierać prezenty;
  • serwer MCP na Node/Next.js, który udostępnia narzędzia:
    • searchGifts — anonimowe, nie wymaga logowania;
    • getSavedGiftLists — prywatne, wymaga uwierzytelnienia;
  • Auth Server (później — Keycloak/Auth0), gdzie każdy użytkownik ma konto.

Scenariusz użytkownika anonimowego i zalogowanego

Jeśli użytkownik po prostu pisze „dobierz prezent dla brata, 30 lat, lubi planszówki”, nasza aplikacja może:

  • wywołać anonimowe narzędzie searchGifts;
  • zwrócić rekomendacje w interfejsie.

W tym przypadku:

  • token nie jest potrzebny;
  • serwer MCP po prostu wykonuje zapytanie (np. do waszego katalogu lub zewnętrznego API).

Gdy tylko użytkownik mówi „zapisz to do moich list” albo „pokaż moje zapisane pomysły”, model decyduje o wywołaniu chronionego narzędzia getSavedGiftLists. Serwer odpowiada 401 + WWW-Authenticate z resource_metadata. ChatGPT uruchamia kreator OAuth „Link GiftGenius account”, przeprowadza użytkownika przez logowanie i uzyskuje token.

Dalej przy każdym chronionym wywołaniu:

  • serwer MCP widzi już Authorization: Bearer ...;
  • wyciąga userId z tokena;
  • filtruje dane po tym userId.

Dzięki temu możemy:

  • oddzielać dane różnych użytkowników;
  • bezpiecznie pokazywać historię zamówień, listę ulubionych;
  • robić funkcje commerce (później w kursie).

Architektura backendu: middleware + handlers narzędzi

Praktycznie w kodzie Node/Next.js często wygląda to jak łańcuch: „middleware uwierzytelniania → handler logiki narzędzia”. W wykładzie o implementacji handlerów tools podkreślaliśmy, że trzeba przekazywać kontekst: user_id, tokeny, ustawienia.

Fragment kodu może wyglądać tak:

// auth-context.ts
export type AuthContext = {
  userId: string | null;    // null dla anonimowych wywołań
  scopes: string[];
};

Middleware podpinany do wszystkich endpointów MCP:

// mcp-auth-middleware.ts
export async function buildAuthContext(req: Request): Promise<AuthContext> {
  const header = req.headers.authorization || "";
  const token = header.replace(/^Bearer\s+/i, "");

  if (!token) return { userId: null, scopes: [] }; // użytkownik anonimowy

  const claims = await verifyAndDecodeToken(token); // weryfikacja tokenu
  const userId = await mapClaimsToUserId(claims);
  const scopes = (claims.scope || "").split(" ");
  return { userId, scopes };
}

A sam handler narzędzia dostaje ten kontekst:

// tools/getSavedGiftLists.ts
export async function getSavedGiftLists(_args: {}, ctx: AuthContext) {
  if (!ctx.userId) throw new Error("User must be authenticated");

  return db.giftList.findMany({
    where: { ownerId: ctx.userId }
  });
}

Chodzi o to, że handler narzędzia nie wie nic o OAuth ani o PKCE. Po prostu pracuje z „oczywistym” userId. Cała magia OAuth jest ukryta wcześniej: w kliencie MCP i w Auth‑middleware.

7. Schematy wizualne: jak razem żyją Client, Server i Auth

Przepływ omówiliśmy już krok po kroku w rozdziale 3 tekstowo. Czasem łatwiej raz narysować niż siedem razy tłumaczyć, więc teraz pokażemy te same interakcje w postaci dwóch diagramów.

Szkielet interakcji (The Triangle of Trust)

flowchart TD
  U[User] -->|1. Login / zgoda| A[MCP Auth Server]
  U -->|2. Czatuje| C["MCP Client (ChatGPT)"]
  C -->|3. OAuth Flow| A
  C -->|4. Bearer Token| S[MCP Server]
  S -->|5. Data| C

Diagram czyta się tak.

Najpierw użytkownik loguje się przez Auth Server, który w istocie potwierdza jego tożsamość i wydaje token. MCP Client zarządza tym procesem, a potem używa tokena, by zwracać się do serwera MCP. Serwer MCP nie widzi loginu i hasła, widzi tylko token i decyduje, co jest dozwolone.

Przepływ od żądania do odpowiedzi

sequenceDiagram
  participant User
  participant ChatGPT as MCP Client
  participant Auth as Auth Server
  participant MCP as MCP Server

  User->>ChatGPT: "Pokaż moje listy prezentów"
  ChatGPT->>MCP: callTool(getSavedGiftLists) (bez tokenu)
  MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata)
  ChatGPT->>Auth: /authorize + PKCE
  User->>Auth: Wprowadza login/hasło, wyraża zgodę
  Auth-->>ChatGPT: redirect + code
  ChatGPT->>Auth: /token + code_verifier
  Auth-->>ChatGPT: access_token (JWT)
  ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ...
  MCP-->>ChatGPT: JSON z prywatnymi listami
  ChatGPT-->>User: Lista wyrenderowana w widżecie

Ten diagram — to, co powinniście umieć „opowiedzieć z zamkniętymi oczami” na końcu modułu.

8. Trochę głębiej: wiele zasobów, wielu klientów, DCR

Urok tej architektury — skaluje się.

Po pierwsze, możecie mieć wiele serwerów MCP (np. jeden od prezentów, drugi od zamówień) i jeden Auth Server, który wydaje tokeny z różnymi aud/resource. Każdy serwer zasobów ma obowiązek sprawdzać, że token jest przeznaczony właśnie dla niego, inaczej powstaje klasyczny problem „confused deputy”, gdy token dla jednej usługi jest akceptowany przez inną.

Po drugie, możecie mieć wielu klientów:

  • ChatGPT App;
  • własny frontend;
  • aplikację mobilną;
  • integrację partnera przez MCP Gateway.

Wszyscy będą:

  • czytać /.well-known/oauth-protected-resource;
  • poznawać, gdzie jest Auth Server;
  • przechodzić OAuth 2.1 flow;
  • otrzymywać tokeny i wywoływać serwer MCP.

Po trzecie, nowoczesne Auth Servery coraz częściej wspierają Dynamic Client Registration (DCR) — możliwość dynamicznej rejestracji klientów przez API. Specyfikacja MCP właśnie to zakłada: klient (ChatGPT/Jam) może automatycznie rejestrować się na Auth Server przez jego registration_endpoint.

W tym module ważne jest, by rozumieć, że:

  • MCP Client, MCP Server i Auth Server komunikują się przez ustandaryzowane dokumenty discovery i tokeny;
  • nie musicie „na sztywno wpisywać” wszystkich klientów w kodzie backendu;
  • możecie rozbudowywać ekosystem bez łamania istniejącego modelu autoryzacji.

9. Typowe błędy w rozumieniu architektury autoryzacji MCP

Błąd nr 1: „Serwer MCP powinien sam logować użytkownika”.
Czasem programiści próbują wbudować formularz logowania prosto w serwer MCP, a następnie wysyłać login/hasło przez narzędzia. To łamie samą ideę OAuth. Serwer MCP nie powinien widzieć hasła w żadnych okolicznościach. Login i zgoda (consent) — to odpowiedzialność Auth Server. Serwer MCP pracuje wyłącznie z tokenami i ich claims.

Błąd nr 2: Mylenie MCP Client i MCP Server.
Zdarza się, że ChatGPT jest postrzegany jako „część mojego backendu” i próbuje się np. przechowywać w nim jakieś sekrety lub oczekiwać, że sam zweryfikuje prawa dostępu. W rzeczywistości MCP Client jedynie inicjuje OAuth i dołącza tokeny. Weryfikacja tokena i uprawnień — to zadanie serwera MCP, a nie ChatGPT.

Błąd nr 3: „Klucz API w .env zamiast OAuth”.
Klasyczny antywzorzec: zrobić jeden wielki SERVICE_API_KEY, włożyć go do .env serwera MCP i uznać, że problem rozwiązany. W takim wariancie nie ma rozdzielenia uprawnień per użytkownik, nie da się bezpiecznie pokazywać danych prywatnych ani wykonywać zakupów, wszystko dzieje się „w imieniu serwisu”, a nie użytkownika. To całkowicie przeczy celom autoryzacji w ChatGPT Apps.

Błąd nr 4: Ignorowanie audience i resource.
Jeśli serwer MCP przyjmuje dowolny ważny JWT z odpowiednim podpisem i nie patrzy na aud/resource, to każdy token wydany dla innej usługi przez ten sam Auth Server może zostać użyty do wywołania waszych narzędzi. To bezpośrednie naruszenie modelu bezpieczeństwa OAuth. Serwer ma obowiązek sprawdzać, że token jest wydany właśnie dla jego resource.

Błąd nr 5: Mieszanie logiki auth i logiki biznesowej.
Czasem do handlerów narzędzi zaczyna się przenosić całą analizę tokena, weryfikację podpisu, pracę z JWK itd. W efekcie kod staje się kruchy i trudny w utrzymaniu. Znacznie lepiej oddzielić warstwę „weryfikacja tokena, mapowanie na userId” (middleware) od warstwy „właściwa logika narzędzia”, która dostaje już czytelny AuthContext.

Błąd nr 6: Oczekiwanie, że ChatGPT „sam wszystko zrobi” bez .well-known.
Bez poprawnego endpointu /.well-known/oauth-protected-resource klient MCP po prostu nie wie, gdzie jest wasz Auth Server i jakie scopes są potrzebne. Skutek — czat „nie umie się logować”, a programista długo patrzy w puste logi. Prawidłowa droga: serwer MCP jasno ogłasza swoje wymagania autoryzacyjne przez .well-known, klient to czyta i buduje flow.

Błąd nr 7: Zapomniany użytkownik w logice biznesowej.
Czasem, nawet poprawnie konfigurując OAuth i mapowanie tokena na userId, programiści nie używają tego w zapytaniach do bazy: np. zapominają filtrować po ownerId = userId. Wtedy każdy autoryzowany użytkownik może zobaczyć cudze dane. Posiadanie tokena — to tylko pierwszy krok; drugim krokiem zawsze jest poprawne użycie userId i scope w kodzie biznesowym.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION