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-проверки становятся неприменимыми, если сигналов 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/приложение, заголовок WWW-Authenticate: Bearer на сканируемом URL, заголовок Link там же с oauth-authorization-server или openid-configuration, а также найденные метаданные защищённого ресурса или любой Bearer-challenge, замеченный проверкой ресурса.
Одно следствие: профиль API/приложение сам считается сигналом, поэтому с ним отсутствующий документ остаётся 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, сканер только для чтения, который проверяет, может ли ИИ-агент прочитать и вызвать сайт. Сканер серверно рендерит собственный контент как рабочий пример.