JavaRush /Курсы /ChatGPT Apps /Настройка MCP Auth Server: на примере Keycloak

Настройка MCP Auth Server: на примере Keycloak

ChatGPT Apps
10 уровень , 2 лекция
Открыта

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 и обменять его на токен с другого места. Схема такая:

  1. Клиент генерирует случайную строку code_verifier.
  2. Хеширует её (обычно SHA-256) и получает code_challenge.
  3. При редиректе на /authorize отправляет code_challenge и code_challenge_method=S256.
  4. После логина пользователь возвращается с code на redirect URI.
  5. Клиент делает POST /token, передавая code и оригинальный code_verifier.
  6. 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 на этапе авторизации.

В нашем курсе есть два типичных клиента:

  1. MCP Jam/Inspector для отладки. Они обычно работают на http://localhost:PORT/.... Для локального сценария логично разрешить redirect вида:
    • http://localhost:5173/* или конкретный путь, который использует Jam.
  2. 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.

Это важно по двум причинам:

  1. Без openid вы не получите id_token и часть стандартных OIDC‑полей.
  2. Без отдельного кастомного 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.attributeclaim.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’а ситуация меняется:

  1. ChatGPT понимает по .well-known вашего MCP‑ресурса, что он защищён и требует токен.
  2. ChatGPT отправляет пользователя в Keycloak по Authorization Code + PKCE flow.
  3. Пользователь логинится (наш alice).
  4. ChatGPT получает access token, в котором sub, email, mcp:tools и прочие claims.
  5. ChatGPT вызывает инструмент GiftGenius уже с Authorization: Bearer <token>.
  6. 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.

1
Задача
ChatGPT Apps, 10 уровень, 2 лекция
Недоступна
PKCE-генератор и ссылка авторизации для public client
PKCE-генератор и ссылка авторизации для public client
1
Задача
ChatGPT Apps, 10 уровень, 2 лекция
Недоступна
OAuth Playground в Next.js (Authorization Code + PKCE + токены/claims)
OAuth Playground в Next.js (Authorization Code + PKCE + токены/claims)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ