JavaRush /Курси /ChatGPT Apps /Навіщо потрібна автентифікація в ChatGPT App і коротка ев...

Навіщо потрібна автентифікація в ChatGPT App і коротка еволюція OAuth

ChatGPT Apps
Рівень 10 , Лекція 0
Відкрита

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 схема приблизно така:

  1. Спочатку через OAuth ви підтверджуєте, що користувач справді увійшов у ваш Identity Provider (IdP) (наприклад, Keycloak/Auth0), і отримуєте токен з його ідентифікатором. Це автентифікація.
  2. Далі 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.

Нам потрібен спосіб, за якого:

  1. Наш зовнішній IdP (Keycloak, Auth0, Hydra+Kratos тощо) знає реального користувача: логін, електронну пошту, userId, можливо, tenant.
  2. Цей IdP видає короткоживучий токен, який ChatGPT може безпечно передавати MCP‑серверу в HTTP‑заголовку Authorization: Bearer <token>.
  3. MCP‑сервер читає токен і перевіряє підпис, issuer, audience, строк дії та scopes; витягує sub (ідентифікатор користувача) і вже на підставі цього зіставляє користувача зі своїми сутностями (accountId, tenantId).
  4. Ті самі scopes дають змогу тонко керувати правами: один токен дає тільки read:gifts, інший — ще й write:gifts або checkout.
  5. Якщо токена немає або в нього не ті 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 тощо), як ми побачимо в наступних лекціях.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ