JavaRush /Курсы /ChatGPT Apps /Архитектура MCP-авторизации: MCP Client, MCP Server, MCP ...

Архитектура MCP-авторизации: MCP Client, MCP Server, MCP Auth Server

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

1. О чём эта лекция и чего в ней нет

Это будет очень интересная лекция, в этой лекции мы:

  • собираем в голове картинку “треугольника доверия” между MCP Client, MCP Server и MCP Auth Server — с человеком‑пользователем, который стоит “над” этим треугольником как владелец ресурсов;
  • проговариваем flow: кто кому шлёт токен, где пользователь логинится и почему MCP‑сервер никогда не видит его пароль;
  • связываем это с нашим Next.js/MCP backend’ом и будущей настройкой Keycloak/Auth0.

Чего мы не делаем сегодня:

  • не кликаем галочки в Keycloak и не настраиваем конкретный IdP;
  • не пишем полноценную проверку JWT или интроспекцию — это темы следующих лекций (про Auth Server и про MCP Server как защищённый ресурс).

Задача сейчас — чтобы вы могли взять листок, нарисовать стрелки между ChatGPT, вашим сервером и Auth0/Keycloak и без запинки объяснить: где логин, где токен, где данные.

2. Треугольник доверия: MCP Client, MCP Server, MCP Auth Server

Начнём с действующих лиц. Технический “треугольник доверия” образуют MCP Client, MCP Server и MCP Auth Server; пользователь (User) — отдельная роль, владелец ресурсов, который стоит как бы над этим треугольником и даёт согласие на доступ. В специфике MCP и Apps SDK эта архитектура формализована довольно чётко.

User (Resource Owner)

Это человек по ту сторону экрана. Он:

  • заходит в ChatGPT;
  • пишет запрос «покажи мои заказы / мои списки подарков»;
  • соглашается “привязать аккаунт” вашего сервиса к ChatGPT.

Главное: именно он владеет ресурсами (история заказов, профили, списки подарков), и именно он даёт согласие на доступ к ним.

MCP Client

Здесь для нас это:

  • ChatGPT с Apps SDK;
  • иногда — MCP Jam Inspector (при отладке).

MCP Client умеет:

  • читать метаданные вашего MCP‑сервера (через .well-known);
  • запускать OAuth‑flow в браузере пользователя;
  • хранить и прикладывать токены к вызовам MCP‑инструментов.

Важно помнить, что MCP Client — public client. Он не хранит ваш client_secret, поэтому общается с Auth Server’ом как публичное SPA‑приложение: Authorization Code + PKCE.

MCP Server (Resource Server)

Это ваш backend, реализующий MCP:

  • поднимает соединение с ChatGPT;
  • объявляет инструменты (tools), ресурсы, промпты;
  • на каждый вызов инструмента смотрит в заголовок Authorization: Bearer <token>;
  • проверяет токен (подпись, exp, aud, scope) и, если всё ок, выполняет бизнес‑логику.

Принципиальный момент: MCP‑сервер не занимается логином. Он не видит паролей, не рисует форму логина, не отправляет пользователю письмо “подтвердите email”. Он доверяет только криптографически подписанным токенам от Auth Server’а.

MCP Auth Server (Authorization Server / IdP)

Это отдельный сервис аутентификации и авторизации: Keycloak, Auth0, Ory Hydra+Kratos, Okta, Cognito, Azure AD и т.д.

Он отвечает за:

  • UI логина (email/пароль, SSO, 2FA);
  • хранение учётных записей пользователей;
  • выдачу токенов (access token, refresh token);
  • публикацию OAuth/OIDC‑метаданных (/authorize, /token, jwks_uri, /registration и т.п.).

Для MCP он должен поддерживать OAuth 2.1 для public clients (PKCE S256, dynamic client registration и т.п.).

Сводная табличка ролей

Кто Что делает Чего не делает
User Вводит логин/пароль, даёт согласие на доступ к данным Не общается напрямую с MCP Server
MCP Client (ChatGPT/Jam) Инициирует OAuth, хранит токен, вызывает MCP tools Не проверяет пароли, не проверяет подпись токена
MCP Server Проверяет токены, выполняет бизнес‑логику tools Не рисует логин‑форму, не хранит пароли
MCP Auth Server Логинит пользователя, выдаёт токены Не знает про ваши MCP‑инструменты и их бизнес‑логику

Если у вас в голове всё это смешивалось в один “большой сервер, который делает всё” — пора разделить.

3. Как выглядит flow: от “нет токена” до защищённого вызова инструментов

Теперь давайте посмотрим на поток сообщений. В спецификации MCP этот процесс называют “The Flow”: discovery → redirect → code → token → authorized calls.

Шаг 0. Попытка вызвать защищённый инструмент без токена

Пользователь пишет: «Покажи мои сохранённые идеи для подарков».

ChatGPT как MCP Client решает: “для этого нужно вызвать инструмент getUserGiftLists у нашего MCP‑сервера”. Он делает вызов без токена (пользователь же ещё не логинился).

Ваш MCP‑сервер:

  • видит отсутствие или некорректный Authorization‑заголовок;
  • отвечает 401 Unauthorized и добавляет заголовок WWW-Authenticate: Bearer resource_metadata="https://api.giftgenius.com/.well-known/oauth-protected-resource" с ссылкой на метаданные защищённого ресурса (resource metadata, об этом ниже).

Это примерно так (логика, не полный HTTP):

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

ChatGPT видит этот заголовок и понимает: “ага, ресурс защищён OAuth, нужно выполнить OAuth‑flow и привязать аккаунт”.

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

Дальше MCP Client запрашивает у вашего сервера метаданные:

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

Сервер отвечает JSON‑документом с идентификатором ресурса и списком авторизационных серверов, у которых нужно получать токены.

Минимальный пример (мы детально настроим позже, сейчас важна идея):

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

Здесь:

  • resource — канонический ID вашего ресурса; его потом должны использовать как audience или resource при выдаче токена;
  • authorization_servers — список Auth Server’ов, у которых ChatGPT может запросить токен;
  • scopes_supported — какие “права” ваш MCP‑сервер вообще понимает.

Authorization Request: редирект на Auth Server

Получив метаданные, MCP Client идёт на Auth Server. Он открывает в браузере вкладку:

GET https://auth.giftgenius.com/authorize
    ?response_type=code
    &client_id=chatgpt-giftgenius
    &redirect_uri=... (обратный URL MCP Client’а)
    &code_challenge=...
    &code_challenge_method=S256
    &scope=openid gifts.read
    &resource=https://api.giftgenius.com

Пользователь:

  • видит знакомый логин (например, Keycloak или Auth0);
  • вводит логин/пароль, проходит 2FA;
  • подтверждает, что ChatGPT может читать его подарочные списки (scope gifts.read).

Code → Token: обмен кода на токен с PKCE

После успешного логина Auth Server редиректит пользователя обратно к MCP Client’у с code. MCP Client:

  • делает POST на /token;
  • передаёт code и code_verifier (который соответствует code_challenge c шага выше).

Auth Server проверяет PKCE: хэширует code_verifier, сравнивает с изначальным code_challenge. Если всё ок и клиент действительно тот же, кто начинал flow, то:

  • выдаёт короткоживущий access_token (обычно JWT);
  • в нём указывает:
    • sub — ID пользователя в Auth Server’е;
    • aud или resource — ваш MCP‑сервер;
    • scope — разрешённые действия (gifts.read, openid и т.п.).

Authenticated Request: вызов MCP‑инструмента с токеном

Теперь MCP Client готов снова вызвать ваш инструмент, но уже с заголовком:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

MCP‑сервер:

  • проверяет подпись токена (по JWK Auth Server’а) или через introspection;
  • проверяет срок действия (exp);
  • проверяет aud / resource — что токен действительно выдан для https://api.giftgenius.com;
  • смотрит на scope и решает, можно ли вызывать getUserGiftLists.

После этого он уже спокойно идёт в вашу БД по какому‑то userId и возвращает личные списки подарков.

Обратите внимание, что до этого момента мы говорили только про сетевой флоу: как токен получается и доезжает до MCP‑сервера. Дальше важно понять, как из sub и других claims в токене получается конкретный userId в вашей БД — здесь в игру вступает identity bridge.

4. Identity Bridge: как user ChatGPT превращается в userId в вашей БД

Самый интересный кусок архитектуры — это “мост идентичности” (identity bridge). В спецификации MCP прямо подчёркивается: MCP‑сервер не знает про пользователей ChatGPT, он опирается на данные в токене от Auth Server’а.

Схема примерно такая:

flowchart TD
  User[User в ChatGPT] -->|Login/SSO| Auth[Auth Server]
  Auth -->|JWT: sub, email, tenant| MCP[MCP Server]
  MCP -->|userId/tenantId| DB[(Ваша БД)]

Пошагово это выглядит так.

Во‑первых, Auth Server внутри знает своих пользователей: у него есть сущности user, email, id, возможно, tenant, roles. При успешном логине он кладёт эту информацию в токен (в 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"
}

Во‑вторых, MCP Server при проверке токена достаёт эти claims и решает, кто это в его мире. Например:

  • если sub уже есть в таблице User.authProviderId — берём связанный userId;
  • если нет — создаём локальную запись (on‑the‑fly provisioning) и связываем.

Типичный кусочек TypeScript‑кода на стороне MCP‑сервера (упрощённый, без проверки подписи) может выглядеть так:

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;
}

В‑третьих, уже по своему userId MCP‑сервер достаёт всё, что нужно: списки подарков, историю заказов, настройки, тариф.

Таким образом, Auth Server становится “мостом” между внешним миром (ChatGPT, Google, SSO) и вашим внутренним миром (customer_id в БД заказов).

5. Почему нужно разделять Auth Server и MCP Server

Может возникнуть соблазн: “А давайте мой MCP‑сервер сам и логин покажет, и токен выпишет”. Формально это возможно (вы можете встроить внутри него мини‑IdP), но архитектурно это плохая идея. Причины довольно приземлённые.

Во‑первых, безопасность и масштабируемость. Auth Server — это тяжёлая машина: 2FA, social login, политики паролей, блокировка аккаунта, восстановление доступа, аудит входов, возможно, сертификации. Писать это заново в каждом микросервисе (в каждом MCP‑сервере) — путь в ад и PCI‑DSS. Гораздо проще делегировать это Keycloak/Auth0 и просто проверять их токен.

Во‑вторых, сменяемость клиента. Сегодня у вас только ChatGPT. Завтра вы подключите Claude Desktop, свой веб‑фронтенд на Next.js, мобильное приложение. Все они могут использовать один и тот же Auth Server и ту же схему OAuth 2.1, а ваш MCP‑сервер просто продолжит проверять токены. Вам не нужно будет переписывать бизнес‑логику под каждый новый клиент.

В‑третьих, чистота кода. MCP Server в идеале:

  • умеет публиковать /.well-known/oauth-protected-resource;
  • умеет проверять Bearer-токен и доставать из него userId, scopes, tenant;
  • реализует бизнес‑инструменты (orders, gifts, profiles).

Вся UI‑логика логина — формы, верстка, социальные логины — живёт в Auth Server’е и не загромождает backend.

6. Как это выглядит в нашем учебном приложении GiftGenius

Вернёмся к приложению, которое мы тянем через курс. Допустим, у нас есть:

  • ChatGPT App “GiftGenius” с виджетом (Apps SDK), который умеет подбирать подарки;
  • MCP‑сервер на Node/Next.js, который предоставляет инструменты:
    • searchGifts — анонимный, не требует логина;
    • getSavedGiftLists — личный, требует аутентификации;
  • Auth Server (позже — Keycloak/Auth0), где у каждого пользователя есть аккаунт.

Сценарий анонимного и залогиненного пользователя

Если пользователь просто пишет “подбери подарок для брата, 30 лет, любит настолки”, наш App может:

  • вызвать анонимный инструмент searchGifts;
  • выдать в интерфейсе рекомендации.

В этом случае:

  • токен не нужен;
  • MCP‑сервер просто выполняет запрос (например, к вашему каталогу или стороннему API).

Как только пользователь говорит “сохрани это в мои списки” или “покажи мои сохранённые идеи”, модель решает вызвать защищённый инструмент getSavedGiftLists. Сервер отвечает 401 + WWW-Authenticate с resource_metadata. ChatGPT запускает OAuth‑wizard “Link GiftGenius account”, прогоняет пользователя через логин и получает токен.

Дальше при каждом защищённом вызове:

  • MCP Server уже видит Authorization: Bearer ...;
  • достаёт userId из токена;
  • фильтрует данные по этому userId.

Именно благодаря этому мы можем:

  • разделять данные разных пользователей;
  • безопасно показывать историю заказов, список избранного;
  • делать commerce‑функции (позже по курсу).

Архитектура backend’а: middleware + handlers инструментов

Практически в коде Node/Next.js это часто выглядит как цепочка: “middleware аутентификация → бизнес‑хендлер инструмента”. В лекции про реализацию tool‑хендлеров мы уже подчёркивали, что в них нужно передавать контекст: user_id, токены, настройки.

Фрагмент кода может быть таким:

// auth-context.ts
export type AuthContext = {
  userId: string | null;    // null для анонимных вызовов
  scopes: string[];
};

Middleware, который вешается на все 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: [] }; // анонимный пользователь

  const claims = await verifyAndDecodeToken(token); // верификация токена
  const userId = await mapClaimsToUserId(claims);
  const scopes = (claims.scope || "").split(" ");
  return { userId, scopes };
}

А сам хендлер инструмента получает этот контекст:

// 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 }
  });
}

Смысл в том, что tool‑хендлер не знает ничего ни про OAuth, ни про PKCE. Он просто работает с “очевидным” userId. Вся OAuth‑магия спрятана до него: в MCP‑клиенте и в Auth‑middleware.

7. Визуальные схемы: как вместе живут Client, Server и Auth

Мы уже пошагово разобрали флоу в разделе 3 текстом. Иногда проще один раз нарисовать, чем семь раз объяснять, поэтому сейчас покажем те же самые взаимодействия в виде двух диаграмм.

Скелет взаимодействия (The Triangle of Trust)

flowchart TD
  U[User] -->|1. Login / Consent| A[MCP Auth Server]
  U -->|2. Чатится| C["MCP Client (ChatGPT)"]
  C -->|3. OAuth Flow| A
  C -->|4. Bearer Token| S[MCP Server]
  S -->|5. Data| C

Схема читается так.

Сначала пользователь логинится через Auth Server, который по сути подтверждает его личность и выдаёт токен. MCP Client управляет этим процессом и потом использует токен, чтобы обращаться к MCP‑серверу. MCP‑сервер не видит логин‑пароль, он видит только токен и решает, что разрешено.

Поток от запроса к ответу

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

  User->>ChatGPT: "Покажи мои списки подарков"
  ChatGPT->>MCP: callTool(getSavedGiftLists) (без токена)
  MCP-->>ChatGPT: 401 + WWW-Authenticate (resource_metadata)
  ChatGPT->>Auth: /authorize + PKCE
  User->>Auth: Вводит логин/пароль, даёт consent
  Auth-->>ChatGPT: redirect + code
  ChatGPT->>Auth: /token + code_verifier
  Auth-->>ChatGPT: access_token (JWT)
  ChatGPT->>MCP: callTool(getSavedGiftLists) + Authorization: Bearer ...
  MCP-->>ChatGPT: JSON с личными списками
  ChatGPT-->>User: Отрисованный список в виджете

Эта диаграмма — то, что вы должны уметь “рассказать с закрытыми глазами” к концу модуля.

8. Чуть глубже: несколько ресурсов, несколько клиентов, DCR

Чем хороша такая архитектура — она масштабируется.

Во‑первых, у вас может быть несколько MCP‑серверов (например, один про подарки, другой про заказы), и один Auth Server, который выдаёт токены с разными aud/resource. Каждый ресурс‑сервер обязан проверять, что токен действительно предназначен именно для него, иначе получится классическая проблема “confused deputy”, когда токен для одного сервиса принимается другим.

Во‑вторых, у вас может быть много клиентов:

  • ChatGPT App;
  • ваш собственный фронтенд;
  • мобильное приложение;
  • интеграция партнёра через MCP Gateway.

Все они будут:

  • читать /.well-known/oauth-protected-resource;
  • узнавать, где находится Auth Server;
  • проходить OAuth 2.1 flow;
  • получать токены и вызывать MCP‑сервер.

В‑третьих, современные Auth Server’ы всё чаще поддерживают Dynamic Client Registration (DCR) — возможность динамически регистрировать клиентов по API. MCP‑спецификация как раз подразумевает такую возможность: клиент (ChatGPT/Jam) может автоматически регистрировать себя на Auth Server’е по его registration_endpoint.

В этом модуле нам важно понимать, что:

  • MCP Client, MCP Server и Auth Server общаются через стандартизированные discovery‑документы и токены;
  • вам не нужно “жёстко прописывать” всех клиентов в коде backend’а;
  • вы можете достраивать экосистему, не ломая существующую авторизационную модель.

9. Типичные ошибки в понимании архитектуры MCP-авторизации

Ошибка №1: “MCP‑сервер должен сам логинить пользователя”.
Иногда разработчики пытаются встроить форму логина прямо в MCP‑сервер, а затем слать логин/пароль через инструменты. Это ломает саму идею OAuth. MCP‑сервер не должен видеть пароль ни при каких обстоятельствах. Логин и consent — зона ответственности Auth Server’а. MCP‑сервер работает только с токенами и их claims.

Ошибка №2: Путаница между MCP Client и MCP Server.
Бывает, что ChatGPT воспринимают как “часть моего backend’а” и пытаются, например, хранить в нём какие‑то секреты или ожидать, что он сам проверит права доступа. На самом деле MCP Client всего лишь инициирует OAuth и прикладывает токены. Проверка токена и прав — задача MCP‑сервера, а не ChatGPT.

Ошибка №3: “API‑ключ в .env вместо OAuth”.
Классический анти‑паттерн: сделать один большой SERVICE_API_KEY, класть его в .env MCP‑сервера и считать, что задача решена. В таком варианте нет разграничения прав по пользователям, нельзя безопасно показывать личные данные или выполнять покупки, всё делается “от имени сервиса”, а не пользователя. Это полностью противоречит целям авторизации в ChatGPT Apps.

Ошибка №4: Игнорирование audience и resource.
Если MCP‑сервер принимает любой валидный JWT с подходящей подписью и не смотрит на aud/resource, то любой токен, выданный для другого сервиса тем же Auth Server’ом, может быть использован для вызова ваших инструментов. Это прямое нарушение модели безопасности OAuth. Сервер обязан проверять, что токен выдан именно для его resource.

Ошибка №5: Смешивание auth‑логики и бизнес‑логики.
Иногда в tool‑хендлеры начинают протаскивать весь разбор токена, проверку подписи, работу с JWK и т.д. В итоге код становится хрупким и трудно сопровождаемым. Гораздо правильнее отделить слой “проверка токена, маппинг на userId” (middleware) от слоя “собственно, логика инструмента”, который получает уже понятный AuthContext.

Ошибка №6: Ожидание, что ChatGPT “сам всё сделает” без .well-known.
Без правильного эндпоинта /.well-known/oauth-protected-resource MCP‑клиент просто не знает, где находится ваш Auth Server и какие scopes нужны. Результат — чат молча “не умеет логиниться”, а разработчик долго смотрит в пустые логи. Верный путь: MCP‑сервер чётко объявляет свои требования к авторизации через .well-known, клиент читает их и строит flow.

Ошибка №7: Забытый пользователь в бизнес‑логике.
Иногда, даже правильно настроив OAuth и маппинг токена на userId, разработчики не используют это в запросах к БД: например, забывают фильтровать по ownerId = userId. Тогда любой авторизованный пользователь может увидеть чужие данные. Наличие токена — это только первый шаг; вторым шагом всегда идёт корректное использование userId и scope в бизнес‑коде.

1
Задача
ChatGPT Apps, 10 уровень, 1 лекция
Недоступна
Protected Resource Metadata (/.well-known/oauth-protected-resource)
Protected Resource Metadata (/.well-known/oauth-protected-resource)
1
Задача
ChatGPT Apps, 10 уровень, 1 лекция
Недоступна
401 Challenge + WWW-Authenticate для защищённого ресурса
401 Challenge + WWW-Authenticate для защищённого ресурса
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ