1. Czym jest serwer autoryzacji w praktyce i dlaczego wybieramy Keycloak
Zacznijmy od krótkiego przypomnienia: serwer autoryzacji (IdP) — to usługa, która:
- pokazuje użytkownikowi ekran logowania/rejestracji oraz zgodę (consent);
- wydaje tokeny OAuth/OIDC (access_token, id_token, refresh_token);
- publikuje dokument discovery i klucze JWKS, aby serwery zasobów mogły weryfikować te tokeny.
W naszym stosie:
- ChatGPT / MCP Jam działa jako klient OAuth (public client);
- twój MCP‑serwer — jako Resource Server;
- Keycloak — jako serwer autoryzacji.
Dlaczego Keycloak jest wygodny na kurs i w realnym życiu:
- jest open source, łatwo uruchomić lokalnie/w Dockerze;
- ma dość przejrzysty model encji: realm, clients, users, roles;
- w praktyce niemal każdą konfigurację, której nauczysz się w Keycloak, przeniesiesz prawie 1:1 do Auth0/Okta/Cognito: te same idee — client, scopes, redirect URIs, PKCE.
Ważna idea: konfigurujemy nie „Keycloak dla całego projektu”, lecz konkretny Realm pod naszą aplikację ChatGPT. To coś w rodzaju „piaskownicy” do uwierzytelniania właśnie klientów MCP.
Krótko mówiąc, na koniec wykładu będziesz mieć:
- własny realm w Keycloak dla aplikacji ChatGPT;
- skonfigurowanego public‑client z Authorization Code + PKCE;
- minimalny zestaw scopes i claims;
- zrozumienie, jak ten token żyje w twoim serwerze Node‑MCP.
2. Podstawowe encje Keycloak przez pryzmat MCP
Aby później nie gubić się w panelu administracyjnym, uporządkujmy encje.
Realm: przestrzeń ustawień i użytkowników
Realm w Keycloak — to odizolowana przestrzeń z własnymi użytkownikami, klientami, politykami. Wygodna analogia — „wynajęte biuro w biurowcu”: każdy ma własne pomieszczenie, własną listę pracowników i własne zasady wejścia.
Dla kursu i dla twojej pierwszej realnej aplikacji warto stworzyć osobny realm, na przykład giftgenius-mcp lub mcp-course. To pozwala:
- nie dotykać master realm, by przypadkowo nie zepsuć panelu admina;
- ponownie używać ustawień i użytkowników między różnymi środowiskami (dev / staging / prod) poprzez eksport/import realm.
Client: wpis o aplikacji (ChatGPT / MCP Jam)
Client w Keycloak — to nie „użytkownik”, lecz aplikacja, która chodzi do serwera autoryzacji po tokeny. W naszym przypadku to nie twój backend Next.js, lecz właśnie klient MCP: ChatGPT, MCP Jam, ewentualnie osobno twój widżet, jeśli robisz ręczny przepływ OAuth w UI.
Kluczowe pola klienta:
- client_id — identyfikator tekstowy;
- typ (public / confidential / bearer-only);
- włączone przepływy OAuth (Standard Flow, Client Credentials itp.);
- lista dozwolonych redirect URI;
- lista scopes i protocol mappers (claims w tokenie).
Dla ChatGPT/MCP Jam potrzebujemy public client, ponieważ:
- ChatGPT jako klient nie może bezpiecznie przechowywać client_secret;
- MCP Jam jako narzędzie desktopowe/przeglądarkowe działa również w niezaufanym środowisku.
User: realna osoba
User — to „żywy” użytkownik: ma username, hasło, email, atrybuty, grupy, role. Gdy ktoś loguje się przez Keycloak, jego sub i inne dane trafiają do tokenu, który dalej weryfikujesz na serwerze MCP i mapujesz na własne accountId / tenantId.
Na potrzeby naszej demonstracji w zupełności wystarczy:
- jeden–dwóch testowych użytkowników (np. alice@example.com, bob@example.com);
- ewentualnie — kilka atrybutów w rodzaju tenant lub plan, jeśli chcemy pokazać, jak claims z tokenu wpływają na zachowanie tools.
3. Wybór typu klienta: public, PKCE i dlaczego bez sekretu
Przejdźmy do sedna: jak dokładnie skonfigurować klienta w Keycloak pod ChatGPT/MCP.
Public vs Confidential: dlaczego nie client_secret
W klasycznej aplikacji webowej budujesz backend, wkładasz tam client_secret i to właśnie serwer chodzi do IdP po tokeny. To confidential client: może przechowywać sekret.
W świecie ChatGPT jest odwrotnie:
- klientem OAuth jest sama platforma ChatGPT albo narzędzie w rodzaju MCP Jam;
- nie kontrolujesz jego kodu ani środowiska;
- każdy client_secret, który dasz ChatGPT, należy uznać natychmiast za skompromitowany.
Dlatego ChatGPT/Jam działają jako public clients, czyli bez client_secret, a kompensują to PKCE — Proof Key for Code Exchange.
PKCE po ludzku: co robi
PKCE — to jednorazowy „sekret na sesję”. Jego cel to uniemożliwić proste przechwycenie authorization code i wymianę go na token z innego miejsca. Schemat jest taki:
- Klient generuje losowy ciąg code_verifier.
- Haszuje go (zwykle SHA-256) i otrzymuje code_challenge.
- Przy przekierowaniu na /authorize wysyła code_challenge oraz code_challenge_method=S256.
- Po logowaniu użytkownik wraca z code na redirect URI.
- Klient wykonuje POST /token, przekazując code i oryginalny code_verifier.
- Keycloak haszuje verifier, porównuje z challenge i, jeśli wszystko OK, wydaje token.
Ważne dla nas: ChatGPT/MCP Jam robią to za nas. Musimy jedynie w kliencie Keycloak włączyć obsługę Authorization Code + PKCE (S256) i nie wymagać client_secret.
4. Konfiguracja Keycloak krok po kroku dla scenariusza MCP
Ustaliliśmy: dla ChatGPT/MCP Jam potrzebujemy public client z Authorization Code Flow i PKCE (S256), bez client_secret. Zobaczmy teraz, jak ta konfiguracja wygląda w ustawieniach Keycloak.
Załóżmy, że masz już działający Keycloak (kontener Docker, instalację lokalną — nieważne). Interesuje nas logika ustawień, a nie to, gdzie dokładnie klikać.
Tworzymy nowy realm dla aplikacji
Utwórzmy realm giftgenius-mcp: to osobna przestrzeń, gdzie będą:
- użytkownicy właśnie dla aplikacji ChatGPT;
- klienci, przez których ChatGPT/MCP Jam będzie przechodzić OAuth;
- własne polityki haseł i tokenów.
Praktyczna wskazówka: nie mieszaj realm, w którym autoryzujesz pracowników panelu admina, z realm dla klientów ChatGPT. Jest to i bezpieczniejsze, i logicznie prostsze.
Dodajemy użytkownika testowego
Tworzymy użytkownika, dajmy na to alice:
- username: alice;
- email: alice@example.com;
- ustawiamy hasło (dla uproszczenia — bez złożonych polityk);
- opcjonalnie dodajemy atrybut tenantId=demo-tenant albo rolę ROLE_PREMIUM.
Później, w MCP‑serwerze, będziesz mógł zdekodować token, pobrać sub, email, tenantId i powiązać je ze swoim modelem użytkownika.
Tworzymy public client dla MCP Jam / ChatGPT
Teraz najciekawsze — Client.
Na poziomie koncepcyjnym parametry powinny wyglądać tak:
- Client ID: giftgenius-mcp-client (nazwa według uznania);
- Typ: public / Client Authentication off;
- włączony Standard Flow (Authorization Code);
- włączone PKCE z metodą S256;
- skonfigurowane redirect URI;
- skonfigurowane wymagane scopes (openid + twój niestandardowy, np. mcp:tools).
Włączamy Standard Flow i PKCE
Na poziomie pojęciowym:
- włączamy Authorization Code Flow (często nazywa się „Standard Flow Enabled”);
- w sekcji PKCE ustawiamy pkceRequired=true i najczęściej jawnie code_challenge_method=S256.
Dlaczego S256: we współczesnej dokumentacji OAuth 2.1 i zaleceniach OpenAI/Model Context Protocol właśnie S256 jest wspierany jako bezpieczna metoda, plain‑PKCE uważa się za niebezpieczny.
Redirect URI — najbardziej delikatny punkt
Redirect URI musi się zgadzać co do znaku z tym, czego użyje klient. W przeciwnym razie dostaniemy błąd invalid_redirect_uri na etapie autoryzacji.
W naszym kursie są dwa typowe klienty:
- MCP Jam/Inspector do debugowania. Zwykle działają na http://localhost:PORT/.... Dla scenariusza lokalnego sensownie jest pozwolić na przekierowania typu:
- http://localhost:5173/* lub konkretną ścieżkę, której używa Jam.
- ChatGPT / Apps SDK w produkcji. Tutaj redirect URI określa sama platforma. W realnej integracji sprawdzisz aktualną dokumentację OpenAI i wpiszesz właściwy URL, którego ChatGPT użyje jako callback.
W ramach wykładu ważne jest zrozumienie: ChatGPT nie może wziąć dowolnego redirect, musi on się zgadzać z wpisanym w serwerze autoryzacji. Dlatego:
- nigdy nie ustawiaj * i „każdy URL ujdzie”;
- dla lokalnego developmentu dopuszczalne są wildcardy w obrębie localhost, ale nie dla produkcji.
Scopes: minimum, ale wystarczająco
Scopes to mini‑lista uprawnień, o które prosi klient.
Dla naszego scenariusza MCP najczęściej potrzebne są:
- openid — aby włączyć OpenID Connect i otrzymywać id_token z polem sub, czasem email;
- niestandardowy scope, np. mcp:tools, który oznacza „dozwolony dostęp do narzędzi MCP”.
W Keycloak można to zrobić przez Client Scopes:
- zostawić openid;
- wyłączyć domyślnie zbędne scope’y, takie jak profile i email, jeśli ich nie potrzebujesz;
- dodać nowy scope mcp:tools, którym później ograniczysz dostęp do narzędzi na Resource Server.
To ważne z dwóch powodów:
- Bez openid nie otrzymasz id_token i części standardowych pól OIDC.
- Bez osobnego niestandardowego scope nie będziesz w stanie po stronie MCP‑serwera jasno stwierdzić: „ten token można używać do wywołania moich narzędzi”.
5. Konfiguracja tokenów: czas życia, podpis i claims
Przyjrzyjmy się teraz, jakie tokeny Keycloak będzie wydawać i jak je dostroić pod scenariusz MCP.
Czas życia access token
W ustawieniach realm w Keycloak jest sekcja Tokens, w której możesz ustawić:
- Access Token Lifespan;
- Refresh Token Lifespan i inne timeouty.
Dla aplikacji ChatGPT ważne są krótkotrwałe access tokens:
- kilka minut lub godzin — to rozsądna wartość;
- jeśli token wygaśnie, MCP‑serwer odpowiada 401, ChatGPT ponownie uruchamia przepływ OAuth, a użytkownik w razie potrzeby loguje się jeszcze raz.
Idea jak w dokumentacji OpenAI dla Apps SDK: krótki TTL + odnowienie tokenów i możliwość dość szybkiego „wylogowania” użytkownika poprzez odwołanie tokenu po stronie IdP.
Refresh tokens dla klienta ChatGPT z reguły albo nie są tak krytyczne, albo wydawane z krótkim okresem ważności, aby nie utrzymywać wiecznych sesji.
Jakie claims chcemy widzieć w tokenie
Minimalnie potrzebujemy:
- sub — unikalny identyfikator użytkownika w Keycloak;
- iss — kto wydał token (issuer);
- aud — dla jakiego zasobu jest token (używane dalej w MCP‑serwerze);
- exp — czas wygaśnięcia;
- scope — lista scopes.
Dodatkowo często przydatne są:
- email — jeśli chcesz widzieć adres użytkownika;
- tenantId albo podobny claim — dla scenariuszy multi‑tenant;
- roles — do dodatkowej autoryzacji.
W Keycloak konfiguruje się to przez Protocol Mappers:
- standardowe mapery dla email, preferred_username itp.;
- custom mappery dla atrybutów użytkownika (user.attribute → claim.name).
Przykład: maper, który dodaje email jako claim w tokenie, ustawiając user.attribute=email, claim.name=email.
Po stronie MCP‑serwera będziesz mógł pobrać te claims z rozparsowanego JWT i:
- powiązać sub ze swoim accountId;
- użyć tenantId do pobierania wyłącznie danych należących do tego tenant;
- użyć roles do rozgraniczenia bardziej „drobnych” uprawnień.
Podpis tokenu i JWKS
Keycloak domyślnie podpisuje access/id tokeny algorytmem asymetrycznym (zwykle RS256) i publikuje klucze publiczne przez endpoint JWKS z dokumentu OpenID Discovery.
Dla nas to ważne, ponieważ MCP‑serwer może:
- wziąć issuer z tokenu;
- po /.well-known/openid-configuration znaleźć endpoint JWKS;
- pobrać klucz publiczny i zweryfikować podpis tokenu lokalnie.
Tę część omówimy szerzej w wykładzie o MCP‑serwerze jako chronionym zasobie, ale już teraz warto rozumieć, po co Keycloak udostępnia te metadane.
6. Dynamic Client Registration (DCR): kiedy to w ogóle potrzebne
Ten rozdział jest raczej zaawansowany. Do tej pory konfigurowaliśmy klienta „ręcznie” w panelu, i to w zupełności wystarcza, aby uruchomić aplikację. Ale protokół OAuth pozwala klientom rejestrować się dynamicznie przez osobny endpoint.
W kontekście ChatGPT i MCP OpenAI pisze wprost, że platforma może używać Dynamic Client Registration. Tzn. ChatGPT rejestruje się na serwerze autoryzacji „w locie”, przez registration_endpoint z dokumentu discovery.
W Keycloak wygląda to tak:
- włącza się DCR na poziomie realm;
- konfigurujesz politykę: kto może rejestrować nowych klientów i z jakimi grant types/scopes.
Przykładowy JSON dla rejestracji public client z Authorization Code + PKCE i scope openid mcp:tools może wyglądać tak:
{
"clientName": "My ChatGPT App",
"redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
"grantTypes": ["authorization_code"],
"responseTypes": ["code"],
"scope": "openid mcp:tools",
"tokenEndpointAuthMethod": "none"
}
Gdzie tokenEndpointAuthMethod: "none" oznacza właśnie public client bez client_secret.
Na potrzeby kursu wystarczy wiedzieć, że:
- DCR jest przydatne, jeśli klientów jest dużo lub są krótkotrwałe;
- ChatGPT potencjalnie może sam zarejestrować się w twoim IdP;
- ale na początku możesz obejść się statycznym klientem, utworzonym przez UI.
7. Jak to się łączy z naszą aplikacją ćwiczeniową
Przypomnijmy: mamy ćwiczebny MCP‑serwer (np. GiftGenius), który potrafi:
- zwracać listę możliwych prezentów;
- przechowywać jakieś listy życzeń użytkownika;
- później — zaglądać do części commerce, składać zamówienia itd.
Dopóki MCP‑serwer jest otwarty, nie wie, kto do niego puka:
- żądanie z ChatGPT może być logicznie „od Alice” lub „od Boba”, ale MCP‑serwer tego nie rozróżnia;
- nie możesz pokazać prywatnej historii prezentów;
- nie możesz pewnie obciążyć właściwego konta.
Po skonfigurowaniu Keycloak jako serwera autoryzacji sytuacja się zmienia:
- ChatGPT rozumie po .well-known twojego zasobu MCP, że jest on chroniony i wymaga tokenu.
- ChatGPT wysyła użytkownika do Keycloak przez przepływ Authorization Code + PKCE.
- Użytkownik się loguje (nasza alice).
- ChatGPT otrzymuje access token, w którym są sub, email, mcp:tools i inne claims.
- ChatGPT wywołuje narzędzie GiftGenius już z nagłówkiem Authorization: Bearer <token>.
- MCP‑serwer, weryfikując token, rozumie: „Aha, to Alice z sub=... i tenantId=demo-tenant” — i odpowiada odpowiednio.
Tę układankę domkniemy w następnym wykładzie, gdzie zrobimy MCP‑serwer „prawdziwym” resource serverem: zaimplementujemy endpoint metadanych, weryfikację tokenu i powiązanie z użytkownikiem.
8. Kilka małych przykładów praktycznych (nasz stos: TypeScript + Node)
Wszystko poniżej — to nie „jedynie słuszny” sposób, lecz przykład referencyjny, jak może to wyglądać w typowym stosie Node/TypeScript. Jeśli teraz bardziej skupiasz się na klikaniu w Keycloak, możesz ten rozdział tylko przejrzeć i wrócić do niego, gdy będziesz podłączać MCP‑serwer.
Choć konfiguracja Keycloak w większości „klika się” w UI lub robi przez jego Admin REST API, warto pokazać kilka fragmentów kodu wokół tego, aby było jasne, jak będziesz tego używać po stronie MCP‑serwera.
Załóżmy, że mamy już serwer Node.js‑MCP (TypeScript) oparty na oficjalnym SDK.
Konfiguracja autoryzacji (issuer i audience)
Utwórzmy mały moduł authConfig.ts:
// authConfig.ts
export const authConfig = {
issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
audience: 'https://mcp.my-company.com', // URL twojego serwera MCP
requiredScopes: ['mcp:tools'], // minimum, którego oczekujemy w tokenie
};
Tutaj issuer — URL realm Keycloak, audience — identyfikator zasobu (będziemy go jeszcze używać w konfiguracji tokenu i MCP).
Podstawowa weryfikacja JWT przez JWKS
W praktyce najpewniej użyjesz biblioteki w rodzaju jsonwebtoken + jwks-rsa albo gotowych narzędzi z MCP SDK. Prosty szkielet może wyglądać tak:
// verifyToken.ts
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
import { authConfig } from './authConfig';
const client = jwksClient({
jwksUri: `${authConfig.issuer}/protocol/openid-connect/certs`,
});
function getKey(header: any, callback: any) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key?.getPublicKey();
callback(err, signingKey);
});
}
export function verifyAccessToken(token: string): Promise<any> {
return new Promise((resolve, reject) => {
jwt.verify(
token,
getKey,
{
audience: authConfig.audience,
issuer: authConfig.issuer,
},
(err, decoded) => (err ? reject(err) : resolve(decoded)),
);
});
}
Oczywiście obsługa błędów i cache’owanie kluczy powinny być staranniejsze, ale widać ideę: Keycloak publikuje klucze JWKS, my je pobieramy i sprawdzamy podpis.
Sprawdzanie scope i wydobycie tożsamości
W middleware dla narzędzi MCP możesz zrobić coś takiego:
// authMiddleware.ts
import { verifyAccessToken } from './verifyToken';
import { authConfig } from './authConfig';
export async function requireAuth(bearerToken: string) {
const token = bearerToken.replace(/^Bearer\s+/i, '');
const decoded: any = await verifyAccessToken(token);
const scopes = (decoded.scope as string).split(' ');
const hasScope = authConfig.requiredScopes.every(s => scopes.includes(s));
if (!hasScope) {
throw new Error('Insufficient scope');
}
return {
userId: decoded.sub,
email: decoded.email,
tenantId: decoded.tenantId,
};
}
A dalej w handlerach narzędzi MCP będziesz używać userId i tenantId, aby ładować właściwe listy prezentów użytkownika. Same narzędzia już implementowaliśmy w poprzednich modułach — teraz ważne jest tylko zobaczyć, jak token z Keycloak staje się zrozumiałą dla twojego backendu tożsamością.
9. Typowe błędy przy konfiguracji Keycloak jako MCP Auth Server
Błąd nr 1: Użycie confidential client z client_secret.
Czasem z przyzwyczajenia tworzy się klienta typu confidential i próbuje wpisać client_secret do konfiguracji MCP/ChatGPT. W ekosystemie ChatGPT App to nie powinno działać i nie będzie bezpieczne: ChatGPT to public client, nie może przechowywać sekretu. Właściwa droga — public client + PKCE.
Błąd nr 2: Zbyt szerokie scopes domyślnie.
Zostawienie włączonych profile, email i całej masy standardowych scopes — a potem rozdawanie takich tokenów każdemu czatowi — to słaby pomysł. Lepiej zminimalizować: openid i konkretny mcp:tools (albo kilka aplikacyjnych scopes) wystarczą w pierwszych wersjach. To zmniejsza ryzyko wycieku zbędnych danych i czyni zachowanie bardziej przewidywalnym.
Błąd nr 3: Nieprawidłowy redirect URI.
Klasyka: w Keycloak wpisano http://localhost:5173/callback, a MCP Jam używa http://localhost:5173/. Albo odwrotnie. W rezultacie — invalid_redirect_uri i frustrujący debug. Zawsze sprawdzaj dokładną wartość redirect URI w dokumentacji Jam/ChatGPT i wpisuj ją co do znaku.
Błąd nr 4: PKCE nie jest włączone albo włączone z niewłaściwą metodą.
Niektóre wersje Keycloak wymagają osobno włączyć „PKCE required” i wskazać metodę S256. Jeśli tego nie zrobisz, ChatGPT/Jam, który oczekuje PKCE, może dostać błąd invalid_request z uwagą o code_challenge. Koniecznie sprawdzaj ustawienia PKCE dla public‑klientów.
Błąd nr 5: Niewłaściwe lub brakujące claims w tokenie.
Bywa, że w tokenie nie ma sub albo email, bo odpowiedni scope lub protocol mapper nie jest ustawiony. W efekcie na MCP‑serwerze widzisz token, ale nie możesz zmapować go na realnego użytkownika. Rozwiązanie: upewnij się, że potrzebne pola (minimum sub, a najlepiej także email/tenantId) są zmapowane do access/id tokenów.
Błąd nr 6: Zbyt długi TTL dla access tokens.
Z punktu widzenia bezpieczeństwa wydawanie access tokens na dobę/tydzień — to kiepski pomysł. W razie wycieku tokenu atakujący uzyska długotrwały dostęp do zasobu MCP. Lepiej ustawiać access tokens jako krótkotrwałe (minuty lub godziny) i stosować ponowną autoryzację w razie potrzeby.
Błąd nr 7: Mieszanie realm i używanie master.
Czasem pierwsze, co się robi, to tworzy klientów i użytkowników w realm master. Potem do tego samego dorzuca się jeszcze parę projektów — i ostatecznie nic nie jest jasne. Lepiej od razu zakładać osobny realm pod konkretną aplikację/kurs. Ułatwi to życie i tobie, i twojemu DevOps.
GO TO FULL VERSION