OAuth discovery для ШІ-агентів: RFC 9728 і RFC 8414
OAuth discovery для ШІ-агентів: 401 веде до метаданих ресурсу RFC 9728, а ті — до метаданих сервера RFC 8414. 2 JSON-файли, 8 помилок і коли їх не публікувати.

OAuth discovery для ШІ-агентів — це ланцюжок із двох документів із метаданими: захищений API відповідає 401 і вказує на метадані ресурсу за RFC 9728, ті називають сервер авторизації, а метадані цього сервера за RFC 8414 перелічують потрібні агентові ендпоінти. Якщо на вашому сайті ніщо не вимагає токена доступу, виявляти нічого, і публікувати не потрібно жодного з документів.
Це шар після доступності сайту для виклику агентами. MCP Server Card або каталог API кажуть агентові, де живе ваш API, але не як у нього увійти. Захищений API має відповісти на це в машиночитаному вигляді. Підтримка залежить від клієнта: у MCP вона обов'язкова для MCP-клієнтів, а інший агент може натомість використати облікові дані, які налаштувала людина.
Ключові висновки
- Ланцюжок:
401ізWWW-Authenticate: Bearer resource_metadata="…", далі/.well-known/oauth-protected-resource(RFC 9728, квітень 2025), далі/.well-known/oauth-authorization-server(RFC 8414, червень 2018) або OpenID Connect Discovery.- Кожен документ називає сам себе (
resource,issuer). Якщо значення не ідентичне ідентифікатору, з якого побудовано URL, клієнт НЕ ПОВИНЕН (MUST NOT) використовувати дані.- Специфікація авторизації MCP (ревізія 2026-07-28, поточна станом на 2 жовтня 2026 року) вимагає від серверів, що використовують авторизацію, реалізувати RFC 9728 (MCP), а від клієнтів — відмовлятися від сервера авторизації без PKCE (security considerations).
- Публічним сайтам лише для читання, власним логінам і API на ключах не потрібен жоден із документів. Підробка гірша за чесний
404.- Наше власне спостереження за кодом перевірок aiSiteReady: обидві OAuth-перевірки отримують статус N/A, якщо сигналів OAuth немає, а сама лише MCP-картка таким сигналом не вважається.
Що таке OAuth discovery і навіщо він ШІ-агентам?
OAuth discovery — це спосіб, у який клієнт дізнається від самого сервера, де отримати токен: RFC 9728 описує захищений ресурс, RFC 8414 — сервер авторизації. Агент на захищеному ендпоінті не має ні вкладки браузера, ні сторінки документації, ні листа з онбордингом — лише HTTP-відповідь, тож наступний крок має нести сама відповідь. Документ ресурсу називає його ідентифікатор, довірені сервери авторизації та scope; документ сервера — issuer, ендпоінти та можливості (RFC 9728, RFC 8414).
RFC 9728 прямо описує послідовність. Клієнт звертається без токена. Ресурс відповідає заголовком WWW-Authenticate з URL метаданих. Клієнт перевіряє ці метадані, будує URL метаданих сервера авторизації зі вказаного issuer, запитує їх, проходить авторизацію й повторює запит уже з токеном (RFC 9728, розділ 5).
Крок 1 — заголовок у відповіді 401. Він несе й параметр scope (RFC 6750, розділ 3), який MCP-серверам СЛІД (SHOULD) зазначати (перенесення рядка — для читабельності):
GET /mcp HTTP/1.1
Host: api.example.com
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp",
scope="scans:read"
RFC 9728 вводить один новий параметр, resource_metadata, а його власний приклад — голий Bearer resource_metadata="…" (RFC 9728, розділ 5.1). MCP допускає заголовок, well-known URI або обидва способи одразу. Клієнти зобов'язані підтримувати обидва й віддавати пріоритет URL із заголовка (MCP, discovery).
Що входить у метадані захищеного ресурсу (RFC 9728)?
Обов'язкове одне поле — resource. Решта в RFC необов'язкова або рекомендована, і саме тут MCP суворіший (RFC 9728, розділ 2).
| Поле | Статус в RFC 9728 | Що повідомляє агентові |
|---|---|---|
resource | REQUIRED | Ідентифікатор ресурсу: URL з https без фрагмента |
authorization_servers | OPTIONAL (у MCP обов'язкове, щонайменше один) | Issuer серверів, що видають токени для цього ресурсу |
scopes_supported | RECOMMENDED | Значення scope для запиту доступу |
bearer_methods_supported | OPTIONAL | Як можна передавати токен: header, body або query |
resource_name | RECOMMENDED | Назва для людей |
Специфікація MCP вимагає, щоб документ ОБОВ'ЯЗКОВО містив authorization_servers щонайменше з одним записом, а клієнти передавали токен у заголовку Authorization, а не в рядку запиту (MCP, discovery, MCP, authorization). Тому чесне значення bearer_methods_supported для MCP-сервера — ["header"]. Мінімальний документ для https://api.example.com/mcp:
{
"resource": "https://api.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["scans:read", "scans:write"],
"bearer_methods_supported": ["header"],
"resource_name": "Example API"
}
Він лежить за адресою https://api.example.com/.well-known/oauth-protected-resource/mcp. Суфікс вставляється між хостом і шляхом; завершальний слеш після хоста перед цим видаляється. Ідентифікатор без шляху використовує «голий» /.well-known/oauth-protected-resource.
Що входить у метадані сервера авторизації (RFC 8414)?
RFC 8414 вимагає issuer і response_types_supported. Ендпоінти обов'язкові, якщо хоч один підтримуваний grant ними користується (RFC 8414, розділ 2).
| Поле | Статус в RFC 8414 | Навіщо це агентові |
|---|---|---|
issuer | REQUIRED | URL з https без query і фрагмента; ідентифікатор, з яким документ зобов'язаний збігатися |
authorization_endpoint | REQUIRED, якщо його використовує хоч один grant | Де користувач підтверджує доступ |
token_endpoint | REQUIRED, якщо підтримується не лише implicit | Де код обмінюється на токен |
response_types_supported | REQUIRED | ["code"] для потоку authorization code |
grant_types_supported | OPTIONAL | Опущене поле типово означає ["authorization_code", "implicit"] |
scopes_supported | RECOMMENDED | Scope, які може запросити агент |
registration_endpoint | OPTIONAL | Динамічна реєстрація клієнтів (RFC 7591) |
code_challenge_methods_supported | OPTIONAL | Якщо опущено, PKCE не підтримується |
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"registration_endpoint": "https://auth.example.com/register",
"client_id_metadata_document_supported": true,
"scopes_supported": ["scans:read", "scans:write"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
Важливі дві деталі. code_challenge_methods_supported — єдиний спосіб дізнатися, що сервер підтримує PKCE. А агенти зазвичай публічні клієнти: none — значення RFC 7591 саме для них, тоді як за опущеного token_endpoint_auth_methods_supported типово береться client_secret_basic. Залишайте registration_endpoint, лише якщо досі підтримуєте Dynamic Client Registration.
Де лежить документ, якщо в issuer є шлях?
Для issuer https://auth.example.com/tenant1 документ лежить за адресою /.well-known/oauth-authorization-server/tenant1: RFC 8414 вставляє суфікс між хостом і шляхом issuer. OpenID Connect Discovery натомість дописує суфікс у кінець. Розділ 5 RFC 8414 описує різницю й дозволяє на перехідний період публікувати документ в обох місцях (RFC 8414). MCP-клієнти пробують по черзі: /.well-known/oauth-authorization-server/tenant1, /.well-known/openid-configuration/tenant1, /tenant1/.well-known/openid-configuration (MCP, discovery).
Віддавайте обидва документи як 200 OK із Content-Type: application/json: цього вимагає кожен RFC. RFC 9728 додає, що діє звичайне HTTP-кешування, і радить Cache-Control з max-age (RFC 9728, розділ 7.10). Жоден RFC не згадує CORS, але браузерному клієнтові, що читає документи з іншого origin, потрібен Access-Control-Allow-Origin (MDN). Для публічних метаданих без секретів доречне значення *. Це моя рекомендація, а не вимога. У nginx це виглядає так:
location = /.well-known/oauth-protected-resource/mcp {
default_type application/json;
add_header Cache-Control "public, max-age=3600" always;
add_header Access-Control-Allow-Origin "*" always;
alias /srv/metadata/oauth-protected-resource.json;
}
Як авторизація MCP використовує ці документи?
MCP вимагає цей ланцюжок від кожного HTTP-сервера, що використовує авторизацію: сервер публікує метадані RFC 9728, його сервер авторизації — метадані RFC 8414 або OpenID Connect, а клієнти проходять обома документами. Сама авторизація лишається необов'язковою. Ревізія 2026-07-28 позначена як чинна на сторінці версій станом на 2 жовтня 2026 року і задає такі правила (MCP, authorization).
| Роль | Вимога в ревізії 2026-07-28 |
|---|---|
| MCP-сервер | MUST реалізувати RFC 9728, указавши в authorization_servers щонайменше один сервер |
| MCP-сервер | MUST оголосити метадані через resource_metadata у відповіді 401 або за well-known URI |
| Сервер авторизації | MUST надавати метадані RFC 8414 або OpenID Connect Discovery |
| MCP-клієнт | MUST підтримувати обидва механізми; спершу URL із заголовка, потім well-known URI |
| MCP-клієнт | MUST відкидати метадані, де issuer відрізняється від ідентифікатора, з якого побудовано URL |
| MCP-клієнт | MUST перевіряти PKCE за code_challenge_methods_supported і відмовлятися, якщо поля немає; MUST використовувати S256, коли здатний |
Джерела: discovery і security considerations. Рядок про PKCE пояснює, чому code_challenge_methods_supported — не прикраса: опустіть його, і сумісний клієнт зупиниться ще до екрана згоди.
Реєстрація клієнтів змінювалася між ревізіями. У 2025-11-25 Client ID Metadata Documents були SHOULD, а Dynamic Client Registration (RFC 7591) — MAY, залишеним для сумісності (2025-11-25). Ревізія 2026-07-28 оголошує Dynamic Client Registration застарілою й спрямовує нові реалізації до Client ID Metadata Documents, підтримку яких оголошують через client_id_metadata_document_supported: true. Сервери, які досі підтримують Dynamic Client Registration, і далі оголошують її через registration_endpoint (MCP, client registration).
OAuth 2.1 і Client ID Metadata Documents станом на 2 жовтня 2026 року все ще були Internet-Draft (ревізії 16 і 02; MCP посилається здебільшого на 13 і 00), тож ця частина може знову змінитися.
Посібник про Server Card пояснює, де розташований MCP-ендпоінт і який транспорт він використовує. Два OAuth-документи кажуть, як отримати до нього токен. Картку публікуйте в будь-якому разі, а OAuth-документи — лише якщо сервер захищений.
Які помилки ламають OAuth discovery?
Найчастіше це ідентифікатор, що не збігся, або відповідь, яка виглядає як успіх, але ним не є.
| Помилка | Що ламається | Як виправити |
|---|---|---|
issuer відрізняється від issuer, з якого побудовано URL (завершальний слеш, http, проксі підміняє хост) | Клієнт не має його використовувати (RFC 8414, розділ 3.3); aiSiteReady повідомляє issuer_mismatch і ставить PARTIAL | Будуйте issuer і URL з одного значення конфігурації |
resource немає або він не дорівнює URL, який запитав клієнт | Поле обов'язкове (RFC 9728, розділ 2); розбіжність використовувати не можна (розділ 3.3) | Віддавайте канонічний URL ресурсу |
| Ендпоінти, що відповідають 404 або перенаправляють на сторінку входу | Discovery працює, а потік падає на authorization_endpoint чи token_endpoint | Перевірте через curl кожен указаний URL; приберіть registration_endpoint, якщо реєстрації немає |
200 з HTML-сторінкою на відсутній well-known URL (fallback односторінкового застосунку) | Soft 404: виглядає як успіх, а потім не парситься як JSON. Сканер фіксує непідтримуваний тип вмісту, але не метадані | Віддавайте JSON раніше за fallback-маршрут; для відсутнього документа повертайте справжній 404 |
Хибний Content-Type, наприклад text/plain | Обидва RFC вимагають application/json; сканер позначає unexpected_content_type у метаданих ресурсу | Задайте тип явно, як у блоці nginx |
Поля code_challenge_methods_supported немає | Отже, PKCE не підтримується; MCP-клієнти відмовляються продовжувати | Оголосіть ["S256"], але лише якщо сервер вимагає PKCE |
У code_challenge_methods_supported немає S256 (наприклад, лише plain) | MCP-клієнти зобов'язані використовувати S256, коли здатні; aiSiteReady ставить серверу PARTIAL | Підтримайте й оголосіть S256 |
Суфікс дописано в кінець для oauth-authorization-server | Не знаходиться: RFC 8414 вставляє суфікс, а в кінець його дописує лише OpenID Connect Discovery | Дотримуйтеся порядку пошуку вище |
«Збігається» означає «збігається точно». RFC 8414 порівнює два рядки посимвольно за кодовими точками, без нормалізації, тому https://auth.example.com і https://auth.example.com/ — різні значення (RFC 8414, розділ 4). Наразі aiSiteReady очікує issuer у корені домену без завершального слеша, тож провайдер, який його ставить (Auth0 публікує https://<tenant>.auth0.com/), отримує issuer_mismatch, хоча клієнт, що побудував URL із цього самого рядка, такі метадані приймає.
Як перевірити свій OAuth discovery?
Перевіряйте ззовні, як це робить агент: один запит за challenge і по одному на кожен документ, за публічними URL.
# 1. Challenge: чи вказує захищений ендпоінт на свої метадані?
curl -sS -D - -o /dev/null https://api.example.com/mcp | grep -i -E '^(HTTP|www-authenticate)'
# 2. Метадані ресурсу: статус, тип вмісту, тіло
curl -sS -D - https://api.example.com/.well-known/oauth-protected-resource/mcp
# 3. Сервер авторизації з authorization_servers
curl -sS -D - https://auth.example.com/.well-known/oauth-authorization-server
Перша команда має вивести приблизно таке (перенесення рядка — для читабельності):
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource/mcp",
scope="scans:read"
Дві інші мають повернути 200 з application/json.
Потім перевірте ідентифікатори — саме там ховається більшість реальних поломок. Цей smoke-тест для CI починає з URL ресурсу, а не з 401, і далі йде ланцюжком: застосовує правило вставки шляху, пробує адреси OpenID Connect, якщо адреса RFC 8414 не відповіла, і точно порівнює resource та issuer:
#!/usr/bin/env bash
set -euo pipefail
RESOURCE="${1:?usage: check-oauth.sh https://api.example.com/mcp}"
origin() { sed -E 's#^(https://[^/]+).*#\1#' <<<"$1"; }
# urlpath відкидає завершальний слеш, як і сканер aiSiteReady; RFC 9728 його зберігає
urlpath() { local p; p="$(sed -E 's#^https://[^/]+##' <<<"$1")"; echo "${p%/}"; }
PRM="$(curl -sSf -H 'Accept: application/json' \
"$(origin "$RESOURCE")/.well-known/oauth-protected-resource$(urlpath "$RESOURCE")")"
jq -e --arg r "$RESOURCE" '.resource == $r' <<<"$PRM" >/dev/null \
|| { echo 'FAIL: resource does not match the URL'; exit 1; }
ISSUER="$(jq -er '.authorization_servers[0]' <<<"$PRM")"
AS="$(curl -sf -H 'Accept: application/json' \
"$(origin "$ISSUER")/.well-known/oauth-authorization-server$(urlpath "$ISSUER")" \
|| curl -sf -H 'Accept: application/json' \
"$(origin "$ISSUER")/.well-known/openid-configuration$(urlpath "$ISSUER")" \
|| curl -sSf -H 'Accept: application/json' \
"$(origin "$ISSUER")$(urlpath "$ISSUER")/.well-known/openid-configuration")"
jq -e --arg i "$ISSUER" '.issuer == $i' <<<"$AS" >/dev/null \
|| { echo 'FAIL: issuer mismatch'; exit 1; }
jq -e '.code_challenge_methods_supported | index("S256") != null' <<<"$AS" >/dev/null \
|| { echo 'FAIL: no S256'; exit 1; }
echo OK
Нарешті, негативний тест для сайту, якому нічого публікувати. Відсутній well-known URL має давати справжній 404, а не привітну сторінку:
curl -sS -o /dev/null -w '%{http_code} %{content_type}\n' \
https://example.com/.well-known/oauth-authorization-server
# очікувано: 404 ... підозріло: 200 text/html (сторінка-заглушка застосунку)
Чи потрібен вам OAuth discovery взагалі?
Часто ні. Ці документи описують OAuth-розгортання, тож без нього публікувати нічого. Пройдіть дерево рішень, перш ніж писати JSON.
| Ваша ситуація | Документ RFC 9728 | Документ RFC 8414 |
|---|---|---|
| Публічний контентний сайт лише для читання | Не публікувати | Не публікувати |
| Публічний API або MCP-сервер без входу | Не публікувати | Не публікувати |
| Лише API-ключі або статичні токени | Не публікувати; описати ключі в документації | Не публікувати |
| Лише власний вхід для вашого застосунку | Не публікувати | Не публікувати |
| Захищений API, токени видає зовнішній провайдер | Публікувати, назвавши провайдера | Його віддає провайдер; перевірте його URL |
| Захищений API, сервер авторизації ваш | Публікувати | Публікувати |
Не публікуйте метадані-заглушки заради зеленої перевірки. Обидва RFC існують для того, щоб документ, який не збігається з ідентифікаторами, яких очікує клієнт, можна було відкинути. Вигаданий запис у authorization_servers відправить агента на сервер, який ніколи не видасть йому токен, а 404 чесніший і дешевший у налагодженні.
Є й зворотна пастка: aiSiteReady вважає голий WWW-Authenticate: Bearer на сканованому URL сигналом OAuth. Якщо ви надсилаєте цей заголовок, додайте вказівник resource_metadata. Якщо OAuth у вас немає, не надсилайте його.
Як aiSiteReady перевіряє OAuth discovery?
Цю статтю покривають дві перевірки aiSiteReady з категорії Protocol/API: OAuth protected resource (oauth_protected_resource) та OAuth authorization server (oauth_authorization_server). Обидві належать до опції сканування oauth_discovery. Для профілю Контентний сайт (content_site) категорію вимкнено, а в профілі API/застосунок (api_application) вона дає 40% оцінки (20% у типовому профілі Усі перевірки). Точні перевірки й ваги описано на сторінці методології.
Для сервера авторизації сканер пробує /.well-known/oauth-authorization-server і /.well-known/openid-configuration, додає варіанти зі вставкою та дописуванням, якщо в сканованого URL є шлях, і йде за заголовками Link сканованого URL з відношеннями oauth-authorization-server або openid-configuration. Жоден RFC таких відношень не визначає; сканер приймає їх як підказку.
Для ресурсу він пробує well-known URL зі вставкою шляху та «голий», потім робить один анонімний запит і читає challenge із WWW-Authenticate. З міркувань захисту від SSRF він іде за URL із resource_metadata, лише якщо той HTTPS, на тому самому хості й порту та під /.well-known/oauth-protected-resource. Потім запитує кожен issuer з authorization_servers, до п'яти, за адресами RFC 8414 та OpenID зі вставкою (адресу з дописуванням поки не перевіряє), через SSRF-безпечний fetcher. Віддалений JavaScript не виконується.
Як сканер оцінює кожен документ?
Відсутній або невалідний authorization_servers робить документ ресурсу невалідним: це суворіше за RFC 9728, зате збігається з MCP. Хибний Content-Type записується в докази й не змінює статус; відсутність scopes_supported залишає документ придатним для ланцюжка issuer, але обмежує результат рівнем PARTIAL. Готовим для агентів вважається сервер із валідними за RFC 8414 метаданими, підтримкою потоку authorization code та оголошеними S256 і scopes_supported.
| Статус | Перевірка ресурсу | Перевірка сервера авторизації |
|---|---|---|
| PASS | Валідний документ, немає розбіжності resource, щонайменше один готовий для агентів сервер, є scopes_supported | Щонайменше один готовий для агентів сервер |
| PARTIAL | Знайдено, але: resource не збігся, немає scope, сервер неповний або не пройшов валідацію, або документ невалідний | Валідний, але немає S256 чи scope; розбіжність issuer; або невалідний |
| FAIL | Нічого не знайдено, і є сигнал OAuth | Те саме |
| N/A | Нічого не знайдено, і сигналу OAuth немає | Те саме |
| UNKNOWN | Завантаження не вдалося або заблоковане як бот (таймаут, 403, 429) | Те саме |
Коли відсутній документ отримує статус N/A?
Коли я вбудовував це в пайплайн сканування, міркування було простим. Упевнене «опублікуйте метадані RFC 8414» на сайті без OAuth — хибне спрацювання: воно знижує оцінку за те, чого сайту робити не треба. Тому один крок запускається після всіх перевірок і переписує лише «нічого не знайдено» FAIL, і лише коли ніщо не вказує на OAuth. Результат N/A потім виключається зі знаменника оцінки.
Сигнали: профіль API/застосунок (api_application), заголовок WWW-Authenticate: Bearer на сканованому URL, заголовок Link там само з oauth-authorization-server або openid-configuration, а також знайдені метадані захищеного ресурсу чи будь-який Bearer-challenge, помічений перевіркою ресурсу.
Один наслідок: профіль API/застосунок (api_application) сам є сигналом, тож із ним відсутній документ лишається FAIL навіть в API на ключах. Якщо OAuth у вас немає, скануйте з типовим профілем або виключіть опцію OAuth discovery з користувацького набору перевірок.
Сама лише MCP-картка, A2A-картка чи каталог API сигналом свідомо не вважаються: агентська поверхня може працювати на API-ключах. PASS, PARTIAL і UNKNOWN не переписуються ніколи, тож справжнє розгортання отримує бал за те, що публікує. Скануйте URL самого ресурсу: оскільки resource має збігатися зі сканованим URL, документ для https://api.example.com/mcp, знайдений під час сканування https://example.com, отримає PARTIAL з resource_mismatch — так задумано.
aiSiteReady не публікує жодного з документів. Його well-known-маршрути віддають A2A-картку, каталог API, індекс Agent Skills і MCP Server Card, а для OAuth — нічого. Публічний MCP-сервер задокументовано як Auth: none з тими самими лімітами, що й REST API. Необов'язкові акаунти, типово вимкнені, — це власний вхід із короткоживучими токенами доступу, а не сервер авторизації, де могли б реєструватися сторонні агенти.
3 жовтня 2026 року всі чотири OAuth well-known URL на www.ai-siteready.com відповідали 404, а головна сторінка не надсилала Bearer-challenge, тож жодна з перевірок не знаходить сигналу OAuth: у типовому профілі обидві показують N/A.
Запустіть безкоштовне сканування вашого API чи MCP-URL, щоб побачити, який результат отримує кожен документ, і HTTP-докази до нього — англійською, українською або російською. Цей посібник — частина про вхід у темі доступності для виклику; посібник про каталог API пояснює, як агенти спершу знаходять API, а аудит за 30 хвилин показує, де це місце в повному проході. Для загальної картини почніть із матеріалу про те, що означає готовність до ШІ-агентів.
Якщо коротко: якщо ваш API захищений, опублікуйте 401, що вказує на метадані RFC 9728, і сервер авторизації, чиї метадані RFC 8414 збігаються з його issuer і оголошують S256. Якщо ні — не публікуйте нічого, і нехай за вас говорить 404.
IMozz має 20 років у розробці ПЗ, а останній рік будує продукти за допомогою LLM. Він розвиває aiSiteReady, сканер лише для читання, що перевіряє, чи може ШІ-агент прочитати й викликати сайт. Сканер серверно рендерить власний контент як робочий приклад.