Перейти к содержимому
Аутентификация MCP-серверов: OAuth 2.1, PKCE и DPoP

Аутентификация MCP-серверов на практике: OAuth 2.1 + PKCE, DPoP и типичные ошибки

Поднимаете авторизацию на удалённом MCP-сервере? Проходим весь путь: от discovery через /.well-known до DPoP-привязки токенов — и разбираем ошибки, на которых горят команды.

Редакция Agentic IAM
Журнал об Agentic IAM и NHI
12 мин

Что требует спецификация

Спецификация авторизации MCP определяет, как клиент обращается к защищённому MCP-серверу от имени владельца ресурса. Ключевые положения ревизии 2025-06-18:

  • Авторизация опциональна, но для HTTP-транспорта реализациям следует ей соответствовать. STDIO-транспорт вне её scope: там учётные данные берутся из окружения.
  • MCP-сервер — это resource server OAuth 2.1: он принимает access token и валидирует его. MCP-клиент — OAuth-клиент, а сервер авторизации — отдельная роль, которая может быть совмещена с MCP-сервером или вынесена наружу.
  • Основной поток — Authorization Code grant с PKCE, причём PKCE обязателен для всех клиентов, а не только публичных.
  • Обязательный каркас discovery: сервер авторизации публикует метаданные по RFC 8414, MCP-сервер — метаданные защищённого ресурса по RFC 9728, клиент указывает целевой ресурс по RFC 8707.

Важно не путать ревизии. В спецификации 2025-03-26 discovery строился на «путях по умолчанию» (/authorize, /token, /register) и производном base URL. С июня 2025 года это заменено обязательным RFC 9728 с заголовком WWW-Authenticate — если вы читаете старые туториалы, проверяйте дату.

Шаг 1. Discovery: от 401 к метаданным

Сценарий начинается с того, что клиент без токена вызывает MCP-сервер и получает 401 Unauthorized. Сервер обязан вернуть заголовок WWW-Authenticate с указанием URL метаданных ресурса:

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

Дальше клиент выполняет два запроса:

  1. GET /.well-known/oauth-protected-resource — документ RFC 9728. Обязательное поле authorization_servers содержит список серверов авторизации, которым доверяет этот MCP-сервер. Если их несколько, выбор — за клиентом.
  2. GET /.well-known/oauth-authorization-server — документ RFC 8414 выбранного сервера авторизации: authorization_endpoint, token_endpoint, registration_endpoint, поддерживаемые grant types и code_challenge_methods_supported (нужен S256).

На этом шаге проверьте три вещи: поле issuer в метаданных совпадает с идентификатором сервера авторизации, все endpoint'ы отдаются по HTTPS, а метаданные не закэшированы у вас навсегда — их стоит перечитывать при ошибках конфигурации.

Шаг 2. Динамическая регистрация клиента

MCP-клиент заранее не знает, с какими серверами ему предстоит работать, поэтому спецификация рекомендует RFC 7591 (Dynamic Client Registration). Клиент отправляет POST на registration_endpoint:

{
  "client_name": "research-agent",
  "redirect_uris": ["http://localhost:8974/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none"
}

Публичный клиент (локальный агент, CLI, браузерное приложение) регистрируется с token_endpoint_auth_method: "none" и не получает client_secret. Если сервер авторизации не поддерживает DCR, остаются два легальных варианта: зарегистрировать клиента вручную и зашить client_id в конфигурацию, либо показать пользователю UI для ввода реквизитов, полученных через консоль сервера.

Практическая рекомендация оператору сервера: включайте DCR с политиками — allowlist по redirect_uris, ограничение scope при регистрации и rate limiting, иначе регистрационная точка станет источником мусорных клиентов.

Шаг 3. Authorization Code + PKCE

Клиент генерирует code_verifier (случайная строка 43–128 символов), вычисляет code_challenge = base64url(SHA256(code_verifier)) и открывает в браузере authorization request:

GET /authorize?
  response_type=code
  &client_id=<client_id>
  &redirect_uri=http%3A%2F%2Flocalhost%3A8974%2Fcallback
  &state=<случайное-значение>
  &code_challenge=<challenge>
  &code_challenge_method=S256
  &resource=https%3A%2F%2Fmcp.example.com

Обратите внимание на параметр resource. По RFC 8707 он обязателен и в authorization request, и в token request, и должен содержать канонический URI MCP-сервера: со схемой, без фрагмента, в нижнем регистре для схемы и хоста и, как правило, без завершающего слэша — например, https://mcp.example.com или https://mcp.example.com/server/mcp, если путь различает отдельные серверы. Именно этот параметр привязывает выданный токен к конкретному MCP-серверу и закрывает целый класс атак на повторное использование токена.

После согласия пользователя сервер возвращает code на redirect_uri. Клиент проверяет state (несовпадение — отбросить ответ) и обменивает код:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=http%3A%2F%2Flocalhost%3A8974%2Fcallback
&code_verifier=<verifier>
&resource=https%3A%2F%2Fmcp.example.com

Требования к redirect URI жёсткие: только localhost или HTTPS, и сервер авторизации обязан сравнивать значение с зарегистрированным точно, строка в строку — никаких суффиксных совпадений.

Шаг 4. Обращение к серверу и защита токенов

Токен передаётся в заголовке Authorization: Bearer <token> в каждом HTTP-запросе, даже внутри одной логической сессии. Передача токена в query string запрещена.

Сторона сервера обязана:

  • проверять подпись, iss, exp и aud — токен должен быть выпущен именно для этого MCP-сервера (см. требования к audience в RFC 9068);
  • отвечать 401 на невалидный или истёкший токен, 403 на недостаток scope, 400 на деформированный запрос;
  • не пробрасывать входящий токен дальше: если MCP-сервер сам вызывает upstream API, он делает это как самостоятельный OAuth-клиент с отдельным токеном от отдельного сервера авторизации. Token passthrough превращает сервер в confused deputy и прямо запрещён спецификацией.

Сроки жизни: access token — короткий (минуты), refresh token для публичных клиентов — с обязательной ротацией при каждом использовании. Хранение: системный keychain или secret store, никогда — plaintext-конфиг, лог-файл или переменная окружения, попадающая в crash report.

Шаг 5. DPoP: привязка токена к ключу клиента

Bearer-токен работает у того, кто его предъявил: украденный токен злоумышленник использует как свой. DPoP (RFC 9449) снимает эту проблему: клиент генерирует асимметричную ключевую пару, и сервер авторизации выпускает токен с утверждением cnf.jkt — отпечатком публичного ключа. Каждый запрос сопровождается свежим DPoP proof — JWT со структурой:

Header:  { "typ": "dpop+jwt", "alg": "ES256", "jwk": <публичный ключ> }
Payload: { "jti": <uuid>, "htm": "POST", "htu": "https://mcp.example.com/mcp",
           "iat": <unixtime>, "ath": <base64url(SHA256(access_token))> }

Сервер проверяет подпись proof, сверяет jkt с привязкой токена, htm/htu — с фактическим запросом, ath — с хешем токена. Без приватного ключа украденный токен бесполезен.

Три практических нюанса:

  • Часы клиента. iat проверяется на свежесть, поэтому расхождение часов (clock skew) ломает валидацию — держите NTP включённым и добавьте разумный допуск.
  • Nonce. Сервер может потребовать актуальный nonce через заголовок DPoP-Nonce — клиент обязан повторить запрос с ним, а не падать с ошибкой.
  • Когда DPoP оправдан. Для высокоценных API, публичных клиентов и сред с риском перехвата — однозначно. Для внутреннего tool-сервера за mTLS-шлюзом издержки (подпись на каждый запрос, управление ключами) могут не окупиться.

Пробел спецификации: куда делся Client Credentials

В ревизии 2025-03-26 Client Credentials Grant прямо упоминался как вариант для случая, когда клиент — приложение, а не человек: агент вызывает MCP-инструмент без имперсонации пользователя. В ревизии 2025-06-18 грант удалён: остался только Authorization Code flow, заточенный под присутствие пользователя.

Для полностью автономного агента это пробел: стандартного способа получить токен без человека в контуре у MCP пока нет. Рабочие обходные пути до появления официального расширения:

  1. Bootstrap человеком. Один интерактивный Authorization Code flow выполняет оператор; дальше агент живёт на refresh token с ротацией. Риски: отзыв или истечение refresh-цепочки требуют повторного bootstrap — этот процесс должен быть задокументирован и мониториться.
  2. Обмен workload identity. Агент предъявляет JWT-SVID (SPIFFE) и обменивает его на access token через RFC 7523 или RFC 8693 — если сервер авторизации поддерживает такой grant. Это самый чистый путь для внутренней инфраструктуры.
  3. Внутренние серверы вне OAuth. Для MCP-серверов в периметре с mTLS и SPIFFE-идентичностями авторизация может вообще не использовать OAuth — спецификация этого не запрещает.
  4. Следить за развитием спецификации. Профили для machine-to-machine сценариев активно обсуждаются сообществом MCP; с высокой вероятностью официальное расширение появится.

Чего делать не стоит: хранить долгоживущий client_secret в теле агента, выпускать «вечные» API-ключи в обход OAuth и пробрасывать чужой пользовательский токен как машинный.

Чек-лист типичных ошибок

ОшибкаСимптомЛечение
Рассинхрон issuer в metadata и токенеТокены не валидируютсяСверить issuer с фактическим iss, включая схему и слэши
Metadata отдаётся по HTTP или с битым TLSDiscovery падает у части клиентовHTTPS обязателен на всех endpoint'ах
Неточное совпадение redirect URIinvalid_redirect_uri при обменеТочная строка: регистр, порт, trailing slash; localhost127.0.0.1
PKCE plain вместо S256Перехват кода становится возможенТолько S256; сервер — отклонять plain
Потеря code_verifier между редиректамиinvalid_grant на token endpointХранить verifier в привязке к state
Clock skewЛожные invalid_token, отказ DPoP proofNTP + допуск 30–60 секунд на exp/iat
Сервер не проверяет audПринимаются токены «для другого сервиса»Строгая проверка audience по RFC 8707
Клиент не шлёт resourceТокен без привязки, отказ совместимых ASВсегда включать канонический URI сервера
401 без WWW-AuthenticateКлиент не находит metadataЗаголовок обязателен по RFC 9728
Токены в логах и конфигахУтечка через observability-стекSecret store, маскирование, короткие TTL
Игнорирование ротации refresh tokenВнезапный logout после ротацииАтомарно заменять refresh token при ответе AS

Минимальный корректный контур безопасности для прода

Сводим всё в один чек-лист внедрения:

  1. HTTPS на всех endpoint'ах сервера авторизации и MCP-сервера.
  2. RFC 9728 metadata + корректный WWW-Authenticate на 401; RFC 8414 metadata у сервера авторизации.
  3. DCR с политиками (allowlist redirect URI, rate limit) или ручная регистрация с фиксированным client_id.
  4. Authorization Code + PKCE S256 + проверка state + точное совпадение redirect URI.
  5. resource indicator от клиента и строгая проверка aud на сервере.
  6. Короткоживущие access tokens, ротация refresh tokens, безопасное хранение.
  7. DPoP для высокоценных операций и публичных клиентов.
  8. Никакого token passthrough: для upstream API — отдельный токен.
  9. Аудит каждого вызова: sub, клиент, ресурс, scope, решение.
  10. Для автономных агентов — задокументированный bootstrap и алертинг на разрыв refresh-цепочки.

Вывод

Авторизация удалённого MCP-сервера — это не изобретение нового протокола, а дисциплинированное применение зрелых стандартов: OAuth 2.1 с PKCE, discovery по RFC 9728/8414, привязка токенов к ресурсу по RFC 8707 и, при необходимости, к ключу клиента по RFC 9449. Главные риски кроются не в криптографии, а в конфигурации: metadata, redirect URI, аудитория токена и хранение секретов. Удаление Client Credentials Grant оставило автономных агентов без стандартного пути — до появления расширения спецификации выбирайте между bootstrap через refresh token и обменом workload identity, и закладывайте в архитектуру возможность перейти на официальный механизм, когда он появится.

Похожие статьи