1. Что такое Auth Server на практике и почему мы берём Keycloak
Начнём с короткого напоминания: Auth Server (IdP) — это сервис, который:
- показывает пользователю экран логина/регистрации и consent;
- выдаёт OAuth/OIDC‑токены (access_token, id_token, refresh_token);
- публикует discovery-документ и JWKS-ключи, чтобы ресурс‑серверы могли эти токены проверять.
В нашем стеке:
- ChatGPT / MCP Jam выступает как OAuth‑клиент (public client);
- ваш MCP‑сервер — как Resource Server;
- Keycloak — как Auth Server.
Почему Keycloak удобен для курса и реальной жизни:
- он open source, легко поднять локально/в Docker;
- у него довольно прозрачная модель сущностей: realm, clients, users, roles;
- по сути, любая конфигурация, которую вы освоите в Keycloak, потом почти 1:1 переносится в Auth0/Okta/Cognito: там те же идеи — client, scopes, redirect URIs, PKCE.
Важная идея: мы настраиваем не «Keycloak для всего проекта», а конкретный Realm под наш ChatGPT App. Это своего рода «песочница» для аутентификации именно MCP‑клиентов.
Короче, в конце лекции у вас будет:
- свой realm в Keycloak под ChatGPT‑приложение;
- настроенный public‑client с Authorization Code + PKCE;
- минимальный набор scopes и claims;
- понимание, как этот токен живет в вашем Node‑MCP‑сервере.
2. Базовые сущности Keycloak через призму MCP
Чтобы потом не теряться в админке, разложим сущности по полочкам.
Realm: пространство настроек и пользователей
Realm в Keycloak — это изолированное пространство с собственными пользователями, клиентами, политиками. Удобная аналогия — «арендованный офис в бизнес‑центре»: у каждого своё помещение, свой список сотрудников и свои правила входа.
Для курса и для вашего первого реального App имеет смысл создать отдельный realm, например giftgenius-mcp или mcp-course. Это позволяет:
- не трогать master realm, чтобы случайно не сломать админку;
- переиспользовать настройки и пользователей между разными окружениями (dev / staging / prod) через экспорт/импорт realm.
Client: запись о приложении (ChatGPT / MCP Jam)
Client в Keycloak — это не «пользователь», а приложение, которое ходит к Auth Server за токенами. В нашем случае это не ваш Next.js‑бэкенд, а именно MCP‑клиент: ChatGPT, MCP Jam, возможно, отдельно ваш виджет, если вы делаете ручной OAuth‑флоу в UI.
Ключевые поля клиента:
- client_id — строковый идентификатор;
- тип (public / confidential / bearer-only);
- включённые OAuth‑потоки (Standard Flow, Client Credentials и т.д.);
- список разрешённых redirect URI;
- список scopes и protocol mappers (claims в токене).
Для ChatGPT/MCP Jam нам нужен public client, потому что:
- ChatGPT как клиент не может безопасно хранить client_secret;
- MCP Jam как десктоп/браузерный инструмент тоже в недоверенной среде.
User: реальный человек
User — это уже «живой» пользователь: у него есть username, пароль, email, атрибуты, группы, роли. Когда кто‑то логинится через Keycloak, именно его sub и другие данные попадают в токен, который вы дальше проверяете на MCP‑сервере и мапите на свои accountId / tenantId.
На уровне нашей демки вполне достаточно:
- одного-двух тестовых юзеров (например, alice@example.com, bob@example.com);
- возможно — пары атрибутов вроде tenant или plan, если хотим показать, как claims из токена влияют на поведение tools.
3. Выбор типа клиента: public, PKCE и почему без секрета
Теперь к сути: как именно настроить клиента в Keycloak под ChatGPT/MCP.
Public vs Confidential: почему не client_secret
В классическом веб‑приложении вы делаете backend, кладёте туда client_secret, и именно сервер ходит в IdP за токенами. Это confidential client: он может хранить секрет.
В мире ChatGPT всё наоборот:
- OAuth-клиентом является сама платформа ChatGPT или утилита вроде MCP Jam;
- вы не контролируете её код и окружение;
- любой client_secret, который вы дадите ChatGPT, нужно считать тут же скомпрометированным.
Поэтому ChatGPT/Jam работают как public clients, то есть без client_secret, и компенсируют это PKCE — Proof Key for Code Exchange.
Что делает PKCE человеческим языком
PKCE — это одноразовый «секрет на сессию». Его цель — чтобы нельзя было просто перехватить authorization code и обменять его на токен с другого места. Схема такая:
- Клиент генерирует случайную строку code_verifier.
- Хеширует её (обычно SHA-256) и получает code_challenge.
- При редиректе на /authorize отправляет code_challenge и code_challenge_method=S256.
- После логина пользователь возвращается с code на redirect URI.
- Клиент делает POST /token, передавая code и оригинальный code_verifier.
- Keycloak хеширует verifier, сравнивает с challenge, и, если всё ок, выдаёт токен.
Важное для нас: всё это за нас делает ChatGPT/MCP Jam. Мы лишь должны в клиенте Keycloak включить поддержку Authorization Code + PKCE (S256) и не требовать client_secret.
4. Пошаговая настройка Keycloak под MCP-сценарий
Итак, мы определились: для ChatGPT/MCP Jam нам нужен public client с Authorization Code Flow и PKCE (S256), без client_secret. Теперь давайте посмотрим, как эта конфигурация выглядит в настройках Keycloak.
Допустим, у вас уже есть работающий Keycloak (Docker‑контейнер, локальная установка — неважно). Сейчас нас интересует логика настроек, а не где именно нажимать.
Создаём новый realm для App
Создадим realm giftgenius-mcp: это отдельная область, где будут:
- пользователи именно для ChatGPT‑приложения;
- клиенты, через которые ChatGPT/MCP Jam будет проходить OAuth;
- свои политики паролей и токенов.
Практический совет: не смешивайте realm, в котором вы авторизуете сотрудников админки, с realm для ChatGPT‑клиентов. Это и безопаснее, и проще логически.
Добавляем тестового пользователя
Создаём пользователя, скажем alice:
- username: alice;
- email: alice@example.com;
- выставляем пароль (для упрощения — без сложных политик);
- при желании добавляем атрибут tenantId=demo-tenant или роль ROLE_PREMIUM.
Позже, в MCP‑сервере, вы сможете декодировать токен, вытащить sub, email, tenantId и связать их со своей моделью пользователя.
Создаём public client для MCP Jam / ChatGPT
Теперь самое интересное — Client.
На концептуальном уровне параметры должны выглядеть так:
- Client ID: giftgenius-mcp-client (имя на ваш вкус);
- Тип: public / Client Authentication off;
- включён Standard Flow (Authorization Code);
- включён PKCE с методом S256;
- настроены redirect URI;
- настроены нужные scopes (openid + ваш кастомный, например, mcp:tools).
Включаем Standard Flow и PKCE
На уровне понятий:
- включаем Authorization Code Flow (часто называется «Standard Flow Enabled»);
- в разделе PKCE указываем pkceRequired=true и чаще всего явно code_challenge_method=S256.
Почему S256: в современной документации OAuth 2.1 и рекомендациях OpenAI/Model Context Protocol именно S256 поддерживается как безопасный метод, plain‑PKCE считается небезопасным.
Redirect URI — самое хрупкое место
Redirect URI должны совпадать побуквенно с тем, что будет использовать клиент. Иначе получим ошибку invalid_redirect_uri на этапе авторизации.
В нашем курсе есть два типичных клиента:
- MCP Jam/Inspector для отладки. Они обычно работают на http://localhost:PORT/.... Для локального сценария логично разрешить redirect вида:
- http://localhost:5173/* или конкретный путь, который использует Jam.
- ChatGPT / Apps SDK в проде. Здесь redirect URI определяет сама платформа. В реальной интеграции вы будете смотреть актуальную документацию OpenAI и прописывать нужный URL, который ChatGPT будет использовать как callback.
В рамках лекции важно именно понимание: ChatGPT не может просто взять любой redirect, он обязан совпадать с записанным в Auth Server. Поэтому:
- никогда не ставьте * и «любой URL пойдёт»;
- для локальной разработки допустимы wildcard’ы в рамках localhost, но не для продакшена.
Scopes: минимум, но достаточно
Scopes — это мини‑список прав, которые запрашивает клиент.
Для нашего MCP‑сценария чаще всего нужно:
- openid — чтобы включить OpenID Connect и получать id_token с полем sub, иногда email;
- кастомный scope, например mcp:tools, который будет обозначать «разрешён доступ к MCP-инструментам».
В Keycloak это можно сделать через Client Scopes:
- оставить openid;
- отключить по умолчанию лишние скопы вроде profile и email, если они вам не нужны;
- добавить новый scope mcp:tools, которым вы потом будете ограничивать доступ к tools на Resource Server.
Это важно по двум причинам:
- Без openid вы не получите id_token и часть стандартных OIDC‑полей.
- Без отдельного кастомного scope вы не сможете на стороне MCP‑сервера чётко сказать: «вот этот токен можно использовать для вызова моих инструментов».
5. Настройка токенов: время жизни, подпись и claims
Теперь посмотрим на то, какие токены Keycloak будет выдавать и как их настроить под MCP‑сценарий.
Время жизни access token
В realm‑настройках у Keycloak есть раздел Tokens, где вы можете задавать:
- Access Token Lifespan;
- Refresh Token Lifespan и другие таймауты.
Для ChatGPT App важны короткоживущие access tokens:
- несколько минут или часов — нормальное значение;
- если токен протух, MCP‑сервер отвечает 401, ChatGPT заново запускает OAuth‑флоу, пользователь при необходимости логинится ещё раз.
Идея та же, что и в документации OpenAI по Apps SDK: короткий TTL + обновление токенов и возможность довольно быстро «разлогинить» пользователя, отозвав токен на стороне IdP.
Refresh tokens для ChatGPT‑клиента, как правило, либо не так критичны, либо выдаются с небольшим сроком, чтобы не держать вечные сессии.
Какие claims мы хотим видеть в токене
Минимально нам нужны:
- sub — уникальный идентификатор пользователя в Keycloak;
- iss — кто выдал токен (issuer);
- aud — для какого ресурса токен (используется далее в MCP‑сервере);
- exp — время истечения;
- scope — список scopes.
Дополнительно часто полезны:
- email — если вы хотите видеть адрес пользователя;
- tenantId или похожий claim — для многотенантных сценариев;
- roles — для дополнительной авторизации.
В Keycloak это настраивается через Protocol Mappers:
- стандартные мапперы для email, preferred_username и т.п.;
- custom mapper’ы для атрибутов пользователя (user.attribute → claim.name).
Пример: маппер, который добавляет email как claim в токен, указывая user.attribute=email, claim.name=email.
На стороне MCP‑сервера вы сможете взять эти claims из распарсенного JWT и:
- связать sub с вашим accountId;
- использовать tenantId для выборки только принадлежащих этому тенанту данных;
- использовать roles для разграничения более «тонких» прав.
Подпись токена и JWKS
Keycloak по умолчанию подписывает access/id токены асимметричным алгоритмом (обычно RS256) и публикует публичные ключи через JWKS endpoint из OpenID Discovery документа.
Для нас это важно потому, что MCP‑сервер сможет:
- взять issuer из токена;
- по /.well-known/openid-configuration найти JWKS endpoint;
- получить публичный ключ и проверить подпись токена локально.
Эту часть мы подробнее разберём в лекции про MCP‑сервер как защищённый ресурс, но уже сейчас полезно понимать, зачем Keycloak отдаёт эти метаданные.
6. Dynamic Client Registration (DCR): когда оно вообще нужно
Этот раздел — скорее продвинутый. До сих пор мы настраивали клиента «руками» в админке, и этого более чем достаточно, чтобы запустить App. Но протокол OAuth позволяет клиентам регистрироваться динамически через отдельный endpoint.
В контексте ChatGPT и MCP OpenAI прямо пишет, что платформа может использовать Dynamic Client Registration. То есть ChatGPT регистрирует себя на Auth Server «на лету», через registration_endpoint из discovery‑документа.
На уровне Keycloak это выглядит так:
- включается DCR на уровне realm;
- вы настраиваете политику: кто может регистрировать новых клиентов и с какими grant types/scopes.
Пример JSON для регистрации public client с Authorization Code + PKCE и scope openid mcp:tools может выглядеть так:
{
"clientName": "My ChatGPT App",
"redirectUris": ["https://jam.proxy.mcpapps.com/callback"],
"grantTypes": ["authorization_code"],
"responseTypes": ["code"],
"scope": "openid mcp:tools",
"tokenEndpointAuthMethod": "none"
}
Где tokenEndpointAuthMethod: "none" означает как раз public client без client_secret.
Для курса достаточно просто знать, что:
- DCR полезен, если клиентов много или они короткоживущие;
- ChatGPT потенциально может сам зарегистрироваться в вашем IdP;
- но на первых порах можно обойтись статическим клиентом, созданным через UI.
7. Как это связано с нашим учебным приложением
Вспомним, что у нас есть учебный MCP‑сервер (например, GiftGenius), который умеет:
- выдавать список возможных подарков;
- хранить какие‑то списки желаний пользователя;
- позже — ходить в commerce‑часть, оформлять заказы и т.п.
Пока MCP‑сервер открыт, он не знает, кто к нему стучится:
- запрос от ChatGPT может быть логически «от Алисы» или «от Боба», но MCP‑сервер этого не различает;
- вы не можете показывать приватную историю подарков;
- вы не можете уверенно списать деньги с нужного аккаунта.
После настройки Keycloak как Auth Server’а ситуация меняется:
- ChatGPT понимает по .well-known вашего MCP‑ресурса, что он защищён и требует токен.
- ChatGPT отправляет пользователя в Keycloak по Authorization Code + PKCE flow.
- Пользователь логинится (наш alice).
- ChatGPT получает access token, в котором sub, email, mcp:tools и прочие claims.
- ChatGPT вызывает инструмент GiftGenius уже с Authorization: Bearer <token>.
- MCP‑сервер, проверяя токен, понимает: «Ага, это Alice с sub=... и tenantId=demo-tenant» — и отвечает соответственно.
Эта связка завершается в следующей лекции, где мы сделаем MCP‑сервер «настоящим» resource server’ом: реализуем endpoint метаданных, проверку токена и привязку к пользователю.
8. Небольшие практические примеры (наш стек: TypeScript + Node)
Всё, что ниже, — не «единственно правильный» способ, а референс того, как это может выглядеть в типичном Node/TypeScript‑стеке. Если вы сейчас больше сосредоточены на кликании Keycloak, этот раздел можно пробежать глазами и вернуться к нему, когда будете подключать MCP‑сервер.
Хотя настройка Keycloak в основном «кликается» в UI или делается через его Admin REST API, полезно показать пару кусочков кода вокруг этого, чтобы было понятно, как вы будете использовать всё это на стороне MCP‑сервера.
Предположим, у нас уже есть Node.js‑MCP‑сервер (TypeScript) на базе официального SDK.
Конфиг авторизации (issuer и audience)
Создадим небольшой модуль authConfig.ts:
// authConfig.ts
export const authConfig = {
issuer: 'https://auth.my-company.com/realms/giftgenius-mcp',
audience: 'https://mcp.my-company.com', // URL вашего MCP-сервера
requiredScopes: ['mcp:tools'], // минимум, что ждём в токене
};
Здесь issuer — URL realm’а Keycloak, audience — идентификатор ресурса (мы ещё будем использовать его в настройке токена и MCP).
Базовая верификация JWT по JWKS
В реальной жизни вы, скорее всего, будете использовать библиотеку наподобие jsonwebtoken + jwks-rsa или готовые утилиты из MCP SDK. Простейший скелет может выглядеть так:
// 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)),
);
});
}
Конечно, обработка ошибок и кэширование ключей должны быть аккуратнее, но идею вы видите: Keycloak публикует JWKS‑ключи, мы их тянем и проверяем подпись.
Проверка scope и извлечение identity
В middleware для MCP‑tools вы сможете сделать что‑то вроде:
// 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,
};
}
И дальше уже в обработчиках MCP‑инструментов вы будете использовать userId и tenantId, чтобы грузить нужные списки подарков пользователя. Сами инструменты мы уже реализовывали в прошлых модулях, сейчас важно лишь видеть, как токен из Keycloak превращается в понятную вашему бэкенду identity.
9. Типичные ошибки при настройке Keycloak как MCP Auth Server
Ошибка №1: Используется confidential client с client_secret.
Иногда по привычке создают клиента с типом confidential и пытаются вписать client_secret в конфиг MCP/ChatGPT. В экосистеме ChatGPT App это работать не должно и не будет безопасно: ChatGPT — public client, он не может хранить секрет. Правильный путь — public client + PKCE.
Ошибка №2: Слишком широкие scopes по умолчанию.
Оставить включёнными profile, email и ещё гору стандартных scopes — и потом раздавать такие токены каждому чату — не лучшая идея. Лучше минимизировать: openid и конкретный mcp:tools (или пару прикладных scopes) — достаточно для первых версий. Это уменьшает риск утечки лишних данных и делает поведение более предсказуемым.
Ошибка №3: Некорректный redirect URI.
Классика: в Keycloak указан http://localhost:5173/callback, а MCP Jam ходит на http://localhost:5173/. Или наоборот. В результате — invalid_redirect_uri и дико фрустрирующий дебаг. Всегда проверяйте точное значение redirect URI в документации Jam/ChatGPT и прописывайте его буква в букву.
Ошибка №4: PKCE не включён или включён не тем методом.
Некоторые версии Keycloak требуют отдельно включить «PKCE required» и указать метод S256. Если этого не сделать, ChatGPT/Jam, который ожидает PKCE, может получить ошибку invalid_request с жалобой на code_challenge. Обязательно проверяйте настройки PKCE для public‑клиентов.
Ошибка №5: Неправильные или отсутствующие claims в токене.
Бывает, что в токене нет sub или email, потому что соответствующий scope или protocol mapper не настроен. В результате на MCP‑сервере вы видите токен, но не можете мапить его на реального пользователя. Решение: убедиться, что нужные поля (минимум sub, а лучше ещё email/tenantId) замаплены в access/id токены.
Ошибка №6: Слишком длинный TTL для access tokens.
С точки зрения безопасности выдавать access tokens на сутки/неделю — плохая идея. В случае утечки токена атакующий получит долгосрочный доступ к MCP‑ресурсу. Лучше делать access tokens краткоживущими (минуты или часы) и использовать повторную авторизацию по необходимости.
Ошибка №7: Путаница с realm и использование master.
Иногда первое, что делают, — создают клиента и пользователей прямо в realm master. Потом туда же прикручивают ещё пару проектов — и, в итоге, непонятно, где кто. Лучше сразу заводить отдельный realm под конкретное приложение/курс. Это упростит жизнь и вам, и вашему DevOps.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ