1. Навіщо взагалі потрібна автентифікація в ChatGPT App
Почнемо з головного: користувач у ChatGPT ≠ користувач у вашому сервісі.
У ChatGPT є власний обліковий запис користувача. У вашого сервісу — свої userId, tenantId, ролі, білінг, замовлення. Між ними за замовчуванням немає жодного «магічного» звʼязку. Якщо ви просто розгорнули MCP‑сервер і описали кілька інструментів (tools), ChatGPT викликатиме їх як певний абстрактний клієнт.
Згадаємо наш умовний приклад: застосунок GiftGenius — ChatGPT App, який допомагає підбирати подарунки та керувати списками бажань. Ось що ми хочемо вміти:
- Показувати користувачеві його збережені списки подарунків.
- Дозволяти позначати подарунки як «придбані» або «отримані».
- Показувати історію замовлень (особливо якщо згодом підемо в commerce/ACP).
Без автентифікації MCP‑сервер узагалі не знає, «хто це». Максимум, що він бачить, — певні технічні ідентифікатори зʼєднання та анонімний subject, який OpenAI дає для ідентифікації й лімітів запитів (rate limits), але при цьому прямо попереджає: його не слід використовувати для авторизації.
Автентифікація vs авторизація
Дуже корисно одразу розмежувати ці два поняття.
- Автентифікація (AuthN) відповідає на запитання: хто це?
- Авторизація (AuthZ) відповідає: що цьому «хтось» дозволено робити?
Для ChatGPT App схема приблизно така:
- Спочатку через OAuth ви підтверджуєте, що користувач справді увійшов у ваш Identity Provider (IdP) (наприклад, Keycloak/Auth0), і отримуєте токен з його ідентифікатором. Це автентифікація.
- Далі MCP‑сервер читає токен, витягує з нього sub, ролі та інші claims і вирішує, чи може цей користувач викликати конкретний інструмент (list_orders, delete_profile тощо). Це авторизація.
На рівні коду це можна уявити так (спрощено):
// Тип даних, який MCP‑сервер має знати про користувача
export interface AuthContext {
userId: string;
roles: string[];
}
// Приклад використання в обробнику tool
async function listGiftLists(auth: AuthContext | null) {
if (!auth) {
throw new Error("Користувач не автентифікований");
}
// Дістаємо з БД лише списки цього користувача
return db.giftLists.findMany({ where: { ownerId: auth.userId } });
}
Без userId і ролей ви просто не зможете коректно писати бізнес‑логіку. Усе перетвориться на «один великий спільний обліковий запис для всіх».
2. Чому «API‑ключ у .env» — не рішення
У нас, як у розробників, є природний імпульс: «Зроблю API‑ключ, покладу в .env — і все запрацює». І справді, для внутрішніх інтеграцій сервіс‑сервіс API‑ключі — нормальний інструмент. Але щойно в гру вступають реальні користувачі та ChatGPT App, підхід «один ключ на всіх» ламається.
Подивімося на типовий код із ранніх модулів, де ми просто зверталися з MCP до свого backend:
// mcp/backendClient.ts
export const backendClient = new BackendClient({
baseUrl: process.env.BACKEND_URL!,
apiKey: process.env.BACKEND_API_KEY!, // один ключ для всього ChatGPT
});
З погляду бекенду тепер усі запити виглядають однаково: «це інтеграція з ChatGPT». Жодної різниці між Марією та Павлом. Отже:
- Неможливо показати «особистий кабінет» — сервер не знає, чий він.
- Неможливо розмежувати права: «цей користувач може лише читати, а цей — ще й купувати».
- Неможливо привʼязати замовлення до конкретної людини у вашій основній системі.
У світі MCP це ще й небезпечно. Специфікація рекомендує використовувати HTTP‑автентифікацію (Bearer, API‑ключі тощо) через Streamable HTTP, але наголошує: повноцінний доступ користувачів до захищених ресурсів краще будувати через OAuth і токени, а не через один сервісний ключ.
Крім того, з погляду політик OpenAI якісний застосунок має запитувати лише ті дані, які справді потрібні, і давати користувачеві контроль над тим, чим він ділиться із застосунком. Це чудово лягає на модель OAuth scopes, але зовсім не поєднується з підходом «один суперключ, який уміє все».
Чим поганий сервісний ключ у контексті ChatGPT
Сервісний API‑ключ виражає ідентичність сервісу, а не користувача. Ним можна підписувати виклики з вашого MCP‑сервера до внутрішніх сервісів або до зовнішніх API (наприклад, OpenAI API), але з ним не можна сказати: «Ось це Василь, покажіть йому його історію замовлень».
Найпростіший анти‑приклад:
// Поганий варіант: «підміна» користувача
async function getMyOrdersFromBackend() {
// MCP‑сервер робить виклик до /orders/me на backend
const res = await fetch(`${BACKEND_URL}/orders/me`, {
headers: {
Authorization: `Bearer ${process.env.BACKEND_API_KEY}`,
},
});
// backend вважає, що "me" — це певний інтеграційний сервіс, а не людина
return res.json();
}
Навіть якщо ви спробуєте штучно вкласти якийсь анонімний userId у тіло запиту, це все одно буде саморобним обхідним шляхом. Вам однаково знадобляться:
- Надійний спосіб довести бекенду: «ось це справді Василь, а не хтось інший».
- Спосіб обмежити права конкретного користувача.
- Механізм відкликання доступу (revoke) для конкретного користувача, а не для всіх одразу.
І ось тут на сцену виходить OAuth.
3. Міні‑словник: чого ми взагалі хочемо від системи входу
Перш ніж занурюватися в історію OAuth, давайте просто сформулюємо вимоги до «нормальної» системи автентифікації для ChatGPT App.
Нам потрібен спосіб, за якого:
- Наш зовнішній IdP (Keycloak, Auth0, Hydra+Kratos тощо) знає реального користувача: логін, електронну пошту, userId, можливо, tenant.
- Цей IdP видає короткоживучий токен, який ChatGPT може безпечно передавати MCP‑серверу в HTTP‑заголовку Authorization: Bearer <token>.
- MCP‑сервер читає токен і перевіряє підпис, issuer, audience, строк дії та scopes; витягує sub (ідентифікатор користувача) і вже на підставі цього зіставляє користувача зі своїми сутностями (accountId, tenantId).
- Ті самі scopes дають змогу тонко керувати правами: один токен дає тільки read:gifts, інший — ще й write:gifts або checkout.
- Якщо токена немає або в нього не ті scopes, сервер може повернути помилку з _meta["mcp/www_authenticate"], щоб ChatGPT показав користувачеві UI авторизації та/або повторно отримав токен.
Загалом нам потрібен стандартний, перевірений часом протокол, який усе це забезпечує. Заздалегідь скажу: це OAuth 2.1 (і його старші/молодші «родичі»).
4. Коротка еволюція OAuth: від динозаврів до PKCE
Тепер акуратно пройдемося еволюцією OAuth — без глибокого занурення в RFC, зате з розумінням, чому нас цікавлять саме сучасні підходи.
OAuth 1.0 / 1.0a: «криптофітнес»
Історично першим зʼявився OAuth 1.0. Він дозволяв вебсайтам надавати іншим сервісам доступ до своїх ресурсів без передавання пароля користувача (що вже непогано). Але:
- Підписи запитів були складними: HMAC‑підписування майже кожного запиту, крутяться base‑рядки, нормалізація параметрів.
- Кожен запит треба було підписувати, зберігати consumer secret і вміти правильно формувати підпис.
Більшість сучасних розробників не прагнуть вручну повторювати всі ці «танці з бубном».
Специфікація 1.0a виправила деякі вразливості, але загальна громіздкість залишилася.
OAuth 2.0: фреймворк, а не «один протокол»
OAuth 2.0 сильно спростив життя: замість однієї чітко прописаної схеми зʼявився набір flows (authorization code, implicit, resource owner password, client credentials тощо). Це дало гнучкість, але водночас спричинило «зоопарк» реалізацій.
Плюси:
- Простіше інтегрувати SPA, мобільні та серверні застосунки.
- Зʼявилося чітке розділення ролей: Resource Owner, Client, Resource Server, Authorization Server.
Мінуси:
- У реальному світі зʼявилося багато небезпечних «коротких шляхів». Flow implicit (який видавав токен прямо в браузер без серверного обміну за кодом) виявився небезпечним.
- Flow password grant (коли клієнт просто надсилає логін/пароль користувача в обмін на токен) суперечить самій філософії OAuth — і став анти‑патерном.
Специфікація сама по собі залишила забагато варіантів «на вибір». Тому зʼявилося багато рекомендацій і найліпших практик, які жили в окремих RFC і блогах.
OAuth 2.1: зібралися, видихнули, навели лад
OAuth 2.1 — це спроба зафіксувати найліпші практики, які на той момент уже сформувалися в спільноті:
- Фокус майже цілком на Authorization Code Flow як на основному робочому варіанті.
- В обовʼязковому порядку використовується PKCE (Proof Key for Code Exchange) для public‑клієнтів — тих, хто не може зберігати секрет (наприклад, мобільні застосунки, SPA і… ChatGPT/MCP‑клієнти).
- Застарілі й небезпечні флоу на кшталт implicit і password grant просто виключені зі специфікації.
- Рекомендації щодо короткого строку життя access token і використання refresh‑токенів для довготривалих сесій.
Чому це важливо для вас? Тому що екосистема навколо MCP і ChatGPT явно орієнтується саме на ці найліпші практики: Apps SDK і специфікація MCP Authorization прямо вимагають авторизаційний код + PKCE, короткоживучі токени та нормальні scopes.
5. Чому у світі ChatGPT App ми мислимо патернами OAuth 2.1 + PKCE
Тепер, коли маємо історичний контекст, подивімося на це крізь призму ChatGPT і MCP.
ChatGPT як public client
ChatGPT (і такі клієнти, як MCP Jam) щодо вашого Auth Server — це типовий public client:
- У нього немає і не може бути надійно збереженого client_secret.
- Він запускається в інфраструктурі OpenAI, яку ви не контролюєте.
Тому єдиний адекватний вибір — Authorization Code Flow + PKCE, де безпека ґрунтується не на секреті клієнта, а на перевірці code challenge і code verifier.
Офіційна документація Apps SDK прямо каже: ChatGPT, виступаючи як MCP‑клієнт, виконує flow з Authorization Code + PKCE (S256) і відмовиться завершувати авторизацію, якщо ваш Authorization Server не оголошує підтримку PKCE в метаданих: code_challenge_methods_supported: ["S256"].
Як виглядає флоу з точки зору MCP
Дуже грубо, але корисно уявити це так (послідовність для захищеного ресурсу):
sequenceDiagram
participant U as Користувач
participant C as ChatGPT (MCP Client)
participant AS as Auth Server
participant RS as MCP Server (Resource)
U->>C: "Покажи мої замовлення"
C->>RS: call_tool(list_orders) без токена доступу
RS-->>C: Помилка + _meta["mcp/www_authenticate"]
C->>AS: Відкриває login/consent (Authorization Code + PKCE)
U->>AS: Логіниться і надає згоду (scopes)
AS-->>C: Authorization Code
C->>AS: Обмін коду на Access Token (+перевірка PKCE)
AS-->>C: Access Token (Bearer)
C->>RS: call_tool(list_orders) з Authorization: Bearer <token>
RS->>RS: Перевірка підпису, issuer, audience, scopes
RS-->>C: Список замовлень користувача
C-->>U: Показує дані
Сервер при цьому використовує:
- Метадані захищеного ресурсу (/.well-known/oauth-protected-resource) — там він оголошує себе як ресурс і вказує, який Authorization Server обслуговує цей ресурс.
- Токен, який приходить у заголовку Authorization: Bearer <token>, або перевіряється як JWT за JWK, або інтроспектується через Auth Server.
- Якщо токен не підходить за audience або scopes, сервер може відхилити запит і знову віддати WWW-Authenticate‑челендж у _meta["mcp/www_authenticate"], щоб ChatGPT повторно пройшов авторизацію з потрібними параметрами.
З погляду вашого коду все це виглядає доволі гуманно: ви отримуєте на вхід уже перевірений AuthContext і працюєте з ним.
Міні‑приклад: як MCP‑tool відрізняє анонімного й автентифікованого користувача
Поки без конкретного OAuth‑SDK — просто концепт:
import type { McpToolHandler } from "./types";
export const listOrders: McpToolHandler = async (_args, context) => {
const auth = context.auth; // припустімо, сюди ми кладемо результат перевірки токена
if (!auth) {
return {
content: [{ type: "text", text: "Потрібно увійти, щоб побачити замовлення." }],
_meta: {
// Челендж для ChatGPT: запусти OAuth‑флоу
"mcp/www_authenticate": [
'Bearer resource_metadata="https://mcp.giftgenius.app/.well-known/oauth-protected-resource", error="insufficient_scope", error_description="Login required to view orders"'
]
},
isError: true
};
}
const orders = await db.orders.findMany({ where: { userId: auth.userId } });
return {
content: [{ type: "text", text: `Знайдено замовлень: ${orders.length}` }],
structuredContent: orders
};
};
Саме така _meta["mcp/www_authenticate"] підказка описана в офіційній документації Apps SDK як тригер для OAuth UI з боку ChatGPT.
6. Що означає «короткоживучий токен, мінімальні scopes» на практиці
Із специфікацій і гайдів випливають іще кілька важливих принципів. Їх варто тримати в голові вже зараз — до наступної лекції про конкретне налаштування IdP.
Короткий строк життя токена
Access token має жити недовго. Навіщо?
- Якщо він витече, зловмисник однаково буде обмежений у часі.
- Ви зможете безпечно змінювати права користувача, і за короткий час токен «протухне» — доведеться запитати новий.
Зазвичай це хвилини або десятки хвилин. Натомість ви отримуєте refresh‑токени та/або повторні авторизації, але в контексті ChatGPT більшу частину рутини бере на себе клієнтська сторона.
Scopes як спосіб обмежити права
Scopes — це рядки на кшталт gifts.read, gifts.write, orders.read, orders.checkout. Вони вказують, на що саме користувач має права в межах цього ресурсу.
Для ChatGPT App це особливо важливо:
- Ви можете видати токен лише з gifts.read, коли користувач просто переглядає списки бажань.
- А для операцій ACP/Instant Checkout логічно запитувати «жорсткіший» набір прав — наприклад, orders.checkout, і явно підсвічувати це користувачеві.
У MCP‑описі tools уже є можливість оголошувати securitySchemes з конкретними scopes для інструментів, щоб ChatGPT знав, які права потрібні для виклику конкретного tool.
Audience: токен має бути «для цього» MCP‑ресурсу
Ще одна важлива деталь — aud (audience). MCP‑сервер має перевіряти, що токен справді видано для нього, а не для якогось сусіднього сервісу.
У документації Apps SDK прямо сказано, що ChatGPT прокидатиме параметр resource і очікує, що Authorization Server відобразить його в токені (зазвичай у aud), а MCP‑сервер перевірятиме це поле.
Є велика ймовірність, що під час review вашого застосунку йому передадуть підроблені auth_token і перевірять, чи немає дір у вашій реалізації безпеки. Тож краще відразу зробити все правильно.
7. Як це лягає на наш застосунок GiftGenius
Давайте знову зосередимося на нашому навчальному App. Зараз у нас приблизно така картина:
- Є MCP‑tool get_gift_ideas, який за описом отримувача та бюджетом пропонує ідеї подарунків. Це може працювати анонімно.
- Є MCP‑tool save_gift_list, який зберігає список у БД. Хочеться, щоб він був привʼязаний до конкретного користувача.
- Є MCP‑tool list_saved_lists, який показує всі збережені користувачем списки. Це точно вимагає автентифікації.
Віджет показує гарні картки подарунків, дає натискати «зберегти» і «позначити придбаним» — усе це, по суті, фронтенд до захищених інструментів MCP.
На рівні типів це може виглядати так:
// Типізація контексту виклику tool (спрощено)
interface ToolContext {
auth: AuthContext | null;
}
// Приклад захищеного інструмента
async function listSavedGiftLists(_input: {}, context: ToolContext) {
if (!context.auth) {
// Тут буде той самий трюк з mcp/www_authenticate, що й вище
throw new Error("Потрібна автентифікація");
}
return db.giftLists.findMany({
where: { ownerId: context.auth.userId }
});
}
І щойно ви пишете такі функції, стає очевидно: «просто API‑ключ у .env» ніяк не допоможе. Потрібен повноцінний AuthContext, який будується на основі перевіреного OAuth‑токена.
Які частини застосунку можуть працювати анонімно, а які — ні
Хороша вправа перед налаштуванням OAuth — пройтися по функціоналу й чесно розділити його на дві категорії.
Наприклад, у GiftGenius:
Анонімно:
- Генерація ідей подарунків на основі опису.
- Показ прикладів і деморежим із фіктивними даними.
Лише для автентифікованих:
- Перегляд і редагування особистих списків бажань.
- Історія замовлень.
- Будь‑які платіжні операції, Instant Checkout, привʼязка до ACP.
У наступних лекціях ми налаштовуватимемо Auth Server (наприклад, Keycloak або звʼязку Hydra+Kratos) і MCP‑сервер так, щоб токени на ці дії мали потрібні scopes, а MCP‑tools уміли коректно відмовляти й просити ChatGPT пройти повторну авторизацію.
8. Типові помилки в розумінні автентифікації в ChatGPT App
Помилка № 1: «ChatGPT же вже знає користувача, навіщо мені свій логін?»
Багато хто думає: «У ChatGPT є обліковий запис користувача, чому б мені просто не використовувати його як userId?». Але ChatGPT не розкриває вам реальну ідентичність користувача і не дає доступу до своїх облікових записів. У MCP‑метаданих ви бачите хіба що анонімний _meta["openai/subject"], який призначений для лімітів запитів та ідентифікації сесії, але при цьому прямо вказано, що його не можна використовувати для авторизації або привʼязки до реальних облікових записів.
Помилка № 2: «Один API‑ключ на всіх — нормально, це ж просто “інтеграція”»
Підхід «вшили в MCP‑сервер API‑ключ до свого бекенду й радіємо» працює лише для сценаріїв, де всі користувачі ChatGPT ділять один і той самий обліковий запис у вашому сервісі. Щойно зʼявляються персональні дані, commerce, ACL — ви впираєтеся в неможливість розрізняти користувачів і керувати їхніми правами. API‑ключ — це ідентичність сервісу, а не користувача.
Помилка № 3: «Давайте зробимо password grant, це найпростіше»
Звичка передавати логін/пароль користувача у ваш бекенд і обмінювати його на токен (Resource Owner Password Credentials Grant) — це застарілий і небезпечний патерн із ранніх часів OAuth 2.0. У сучасних рекомендаціях і в контексті OAuth 2.1 його вважають анти‑патерном. Публічні клієнти на кшталт ChatGPT узагалі не повинні бачити паролі ваших користувачів — для цього й існує Authorization Code + PKCE.
Помилка № 4: «PKCE — це зайва складність, давайте без нього»
PKCE (особливо S256) — це не модний маркетинг, а обовʼязковий механізм захисту Authorization Code Flow для public‑клієнтів. Без PKCE викрадений authorization code можна використати повторно. У специфікації MCP Authorization і в Apps SDK прямо зазначено, що ChatGPT вимагає оголосити підтримку PKCE в метаданих Authorization Server і використовує саме цей механізм. Якщо ви його вимкнете, флоу просто не запрацює.
Помилка № 5: «Давайте одразу запросимо всі можливі scopes — про всяк випадок»
Іноді хочеться зробити токен із правами «відкрити й навіть диск C: відформатувати». Але це порушує принцип мінімізації прав (PoLP) і суперечить політикам як OpenAI, так і більшості IdP. Краще чітко продумати, які scopes реально потрібні вашому ChatGPT App: одні — для читання, інші — для запису, окремі — для commerce. Це не лише підвищує безпеку, а й впливає на UX згоди: користувач бачить зрозумілий і обмежений набір прав, а не лячний список із двадцяти незрозумілих рядків.
Помилка № 6: «MCP‑сервер сам зберігатиме логіни/паролі й малюватиме UI логіна»
MCP‑сервер — це Resource Server, а не Auth Server. Він має вміти перевіряти токени, оголошувати свої .well-known‑метадані та повертати WWW-Authenticate‑челенджі, але не займатися входом і зберіганням паролів. Для login/consent краще використовувати спеціалізований Authorization Server (Keycloak, Hydra, Auth0 тощо), як ми побачимо в наступних лекціях.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ