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

Что требует спецификация
Спецификация авторизации 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"
Дальше клиент выполняет два запроса:
GET /.well-known/oauth-protected-resource— документ RFC 9728. Обязательное полеauthorization_serversсодержит список серверов авторизации, которым доверяет этот MCP-сервер. Если их несколько, выбор — за клиентом.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 пока нет. Рабочие обходные пути до появления официального расширения:
- Bootstrap человеком. Один интерактивный Authorization Code flow выполняет оператор; дальше агент живёт на refresh token с ротацией. Риски: отзыв или истечение refresh-цепочки требуют повторного bootstrap — этот процесс должен быть задокументирован и мониториться.
- Обмен workload identity. Агент предъявляет JWT-SVID (SPIFFE) и обменивает его на access token через RFC 7523 или RFC 8693 — если сервер авторизации поддерживает такой grant. Это самый чистый путь для внутренней инфраструктуры.
- Внутренние серверы вне OAuth. Для MCP-серверов в периметре с mTLS и SPIFFE-идентичностями авторизация может вообще не использовать OAuth — спецификация этого не запрещает.
- Следить за развитием спецификации. Профили для machine-to-machine сценариев активно обсуждаются сообществом MCP; с высокой вероятностью официальное расширение появится.
Чего делать не стоит: хранить долгоживущий client_secret в теле агента, выпускать «вечные» API-ключи в обход OAuth и пробрасывать чужой пользовательский токен как машинный.
Чек-лист типичных ошибок
| Ошибка | Симптом | Лечение |
|---|---|---|
Рассинхрон issuer в metadata и токене | Токены не валидируются | Сверить issuer с фактическим iss, включая схему и слэши |
| Metadata отдаётся по HTTP или с битым TLS | Discovery падает у части клиентов | HTTPS обязателен на всех endpoint'ах |
| Неточное совпадение redirect URI | invalid_redirect_uri при обмене | Точная строка: регистр, порт, trailing slash; localhost ≠ 127.0.0.1 |
PKCE plain вместо S256 | Перехват кода становится возможен | Только S256; сервер — отклонять plain |
Потеря code_verifier между редиректами | invalid_grant на token endpoint | Хранить verifier в привязке к state |
| Clock skew | Ложные invalid_token, отказ DPoP proof | NTP + допуск 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 |
Минимальный корректный контур безопасности для прода
Сводим всё в один чек-лист внедрения:
- HTTPS на всех endpoint'ах сервера авторизации и MCP-сервера.
- RFC 9728 metadata + корректный
WWW-Authenticateна 401; RFC 8414 metadata у сервера авторизации. - DCR с политиками (allowlist redirect URI, rate limit) или ручная регистрация с фиксированным
client_id. - Authorization Code + PKCE
S256+ проверкаstate+ точное совпадение redirect URI. resourceindicator от клиента и строгая проверкаaudна сервере.- Короткоживущие access tokens, ротация refresh tokens, безопасное хранение.
- DPoP для высокоценных операций и публичных клиентов.
- Никакого token passthrough: для upstream API — отдельный токен.
- Аудит каждого вызова:
sub, клиент, ресурс, scope, решение. - Для автономных агентов — задокументированный 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, и закладывайте в архитектуру возможность перейти на официальный механизм, когда он появится.

