OAuth 2.1 нужен приложению, которое работает с API от имени другого человека: MCP-клиенту, редактору, интеграции для чужих аккаунтов. Пользователь нажимает «Разрешить доступ» на экране согласия, и приложение получает токен на его аккаунт, не узнавая ни пароля, ни ключа. Для своих скриптов и своего сервера проще API-ключ.
Поддерживается один поток — код авторизации с PKCE и refresh-токеном. Сервер авторизации и API живут на https://api.coremind.ru, маршруты сервера авторизации — под /oauth/….
Как подключиться
- Прочитайте метаданные сервера авторизации и ресурса.
- Получите client_id — опубликуйте документ с метаданными клиента или зарегистрируйтесь через
/oauth/register. - Отправьте пользователя на авторизацию с PKCE.
- Обменяйте код на access- и refresh-токен.
- Вызывайте /v1 с access-токеном.
- Обновляйте токен по истечении и отзывайте доступ, когда он больше не нужен.
Discovery
Оба документа читаются без токена по фиксированным путям на https://api.coremind.ru.
| Адрес | Что возвращает |
|---|---|
| /.well-known/oauth-authorization-server | Метаданные сервера авторизации (RFC 8414): issuer, адреса authorize, token, revoke, register, поддерживаемые методы и признак поддержки client ID metadata document. |
| /.well-known/oauth-protected-resource/v1 | Метаданные ресурса /v1 (RFC 9728): resource, authorization_servers, scopes_supported, bearer_methods_supported. |
Клиенту, который начинает с вызова API, адрес подскажет сам отказ: любой ответ 401 на /v1 несёт заголовок WWW-Authenticate с resource_metadata, а при негодном токене — ещё и error="invalid_token".
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://api.coremind.ru/.well-known/oauth-protected-resource/v1"Регистрация клиента
Регистрация здесь — это получение client_id, с которым приложение ходит на /oauth/authorize и /oauth/token. Способов два, оба без участия поддержки: client ID metadata document (предпочтительный) и динамическая регистрация.
Клиентский секрет не выдаётся ни в одном. Клиент либо публичный (none, защищён PKCE), либо подписывает запросы к token и revoke своим ключом (private_key_jwt). Методы с общим секретом отклоняются.
Client ID metadata document
Вы публикуете у себя JSON с метаданными клиента, и client_id — это адрес этого JSON. Отдельного шага регистрации нет, и client_id мы не выдаём: сервер скачивает документ, когда приложение впервые к нему обращается (запрос авторизации, обмен кода), проверяет его и кеширует. Разрешение, которое пользователь выдал на экране согласия, сохраняется как у любого приложения — он видит его в профиле и может отозвать.
Поля документа:
| Параметр | Обязателен | Что указывать |
|---|---|---|
| client_id | да | Адрес самого документа, побайтно совпадающий с тем, откуда он скачан. |
| redirect_uris | да | Массив адресов возврата — по правилам redirect URI. |
| client_name | нет | Имя на экране согласия. Без него пользователь увидит client_id. |
| grant_types | нет | authorization_code, refresh_token — оба по умолчанию. Без refresh_token refresh-токен не выдаётся. Остальные значения отбрасываются. |
| response_types | нет | Только ["code"], это и значение по умолчанию. |
| token_endpoint_auth_method | нет | none (по умолчанию) или private_key_jwt. |
| application_type | нет | web (по умолчанию) или native. У native порт loopback-адреса возврата может отличаться от указанного. Клиент, у которого все адреса возврата — loopback http:, считается native независимо от этого поля. |
| jwks | для private_key_jwt | Публичные ключи клиента (JWK Set) — или jwks_uri с адресом на них, но не оба сразу. |
Необязательные client_uri, logo_uri, policy_uri, tos_uri и contacts принимаются, но экран согласия их не показывает.
{
"client_id": "https://app.example.com/oauth/client.json",
"client_name": "Моё приложение",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Требования к адресу и ответу:
- Только
https:, без фрагмента, логина в адресе и сегментов./..; хост разрешается в публичный адрес — внутренние и loopback-адреса отклоняются. - Ответ
200напрямую: редиректы не выполняются. Размер ответа и время скачивания ограничены — держите документ небольшим. - Тело — JSON-объект. Полей
client_secretиclient_secret_expires_atбыть не должно, все URL-поля —https:. - Документ кешируется по
Cache-Control: max-ageилиExpiresв пределах нижней и верхней границы; правка вступает в силу, когда истечёт кеш.
Динамическая регистрация
Для клиентов без поддержки metadata document — POST /oauth/register по RFC 7591, без токена. Тело — JSON:
| Параметр | Обязателен | Что указывать |
|---|---|---|
| redirect_uris | да | Массив адресов возврата — по правилам redirect URI. |
| client_name | нет | Имя на экране согласия. Без него пользователь увидит client_id. |
| application_type | нет | web (по умолчанию) или native. У native порт loopback-адреса возврата может отличаться от зарегистрированного. Клиент, у которого все адреса возврата — loopback http:, считается native независимо от этого поля. |
| grant_types | нет | authorization_code, refresh_token — оба по умолчанию. Без refresh_token refresh-токен не выдаётся. |
| response_types | нет | Только ["code"], это и значение по умолчанию. |
| token_endpoint_auth_method | нет | none (по умолчанию) или private_key_jwt. |
| jwks | для private_key_jwt | Публичные ключи клиента — или jwks_uri, но не оба сразу. |
POST /oauth/register HTTP/1.1
Host: api.coremind.ru
Content-Type: application/json
{
"client_name": "Моё приложение",
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"application_type": "native",
"token_endpoint_auth_method": "none"
}Ответ 201 повторяет принятые метаданные с подставленными значениями по умолчанию. Сохраните client_id — он понадобится в каждом запросе. Токена управления регистрацией нет: изменить её нельзя, только зарегистрироваться заново.
HTTP/1.1 201 Created
Content-Type: application/json
{
"client_id": "<client_id>",
"client_id_issued_at": <unix_time>,
"client_name": "Моё приложение",
"redirect_uris": ["http://127.0.0.1:8765/callback"],
"application_type": "native",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Негодный адрес возврата отклоняется с invalid_redirect_uri, остальные ошибки метаданных — с invalid_client_metadata. Маршрут ограничен по частоте и с одного IP, и в сумме: отказ — 429 с кодом rate_limited и заголовком Retry-After. Регистрируйтесь один раз на установку.
Redirect URI
https:— либоhttp:, но только на loopback:localhost,127.0.0.1,[::1](RFC 8252 §7.3). Другие схемы отклоняются при регистрации.- Без фрагмента (#…).
- В запросе авторизации
redirect_uriобязателен и сверяется с зарегистрированным точно. Незарегистрированный адрес отклоняется сразу — браузер туда не отправляется. - Исключение — клиент с
application_type: "native", а также клиент, у которого все адреса возврата — loopbackhttp:(он считается native автоматически): портhttp:-адреса на loopback может отличаться от зарегистрированного.
Запрос авторизации
Откройте в браузере пользователя:
GET https://api.coremind.ru/oauth/authorize
?response_type=code
&client_id=<client_id>
&redirect_uri=<redirect_uri>
&scope=me:read generations:read generations:write
&resource=https://api.coremind.ru/v1
&state=<state>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256| Параметр | Обязателен | Что указывать |
|---|---|---|
| response_type | да | code |
| client_id | да | Адрес metadata document или выданный client_id. |
| redirect_uri | да | Один из зарегистрированных адресов, символ в символ — кроме порта loopback-адреса у native-клиента (см. Redirect URI). |
| scope | да | Области доступа через пробел, хотя бы одна. offline_access принимается, но не нужен: refresh-токен выдаётся любому клиенту с грантом refresh_token. |
| resource | нет | https://api.coremind.ru/v1 — оно же по умолчанию. Другое значение — ошибка invalid_target. |
| state | рекомендуется | Случайная строка; вернётся без изменений — сверьте её с сохранённой. |
| code_challenge | да | BASE64URL(SHA256(code_verifier)), где code_verifier — случайная строка, которую вы храните до обмена кода. |
| code_challenge_method | да | Только S256. |
Экран согласия
Пользователь входит в свой аккаунт CoreMind, если ещё не вошёл, и видит имя приложения — то, что оно само о себе заявило, — адрес, куда вернётся браузер, и по галочке на каждую запрошенную область. Галочки можно снять, поэтому токен несёт ровно то, что пользователь оставил: читайте scope из ответа token, а не запрошенный набор.
Согласие запоминается. Повторный запрос от того же клиента с теми же или более узкими областями проходит без экрана, более широкий показывает его снова. У пользователя одно действующее разрешение на клиента: новое согласие отзывает прежнее вместе с его токенами.
Исключение — native-клиент (в том числе с одними loopback-адресами возврата): экран согласия показывается при каждой авторизации. Любое приложение на компьютере пользователя может слушать loopback-порт, и только так пользователь видит, кто просит доступ (RFC 8252 §8.6).
Возврат на redirect_uri
После согласия браузер возвращается на redirect_uri с параметрами code, state и iss (всегда https://api.coremind.ru).
HTTP/1.1 303 See Other
Location: <redirect_uri>?code=<code>&state=<state>&iss=https%3A%2F%2Fapi.coremind.ruЕсли авторизация не удалась, вместо code приходят error и error_description:
| error | Когда |
|---|---|
| access_denied | Пользователь нажал «Отклонить»; у аккаунта нет доступа к API или он заблокирован — тогда экран согласия не показывается. |
| invalid_scope | В scope нет ни одной области API. |
| invalid_target | Неизвестный resource. |
| invalid_request | Нет code_challenge или code_challenge_method, либо метод не S256. |
| unsupported_response_type | response_type не code. |
Неизвестный client_id или незарегистрированный redirect_uri на приложение не возвращаются: ошибку увидит пользователь.
Обмен кода на токены
POST /oauth/token, тело application/x-www-form-urlencoded. Код одноразовый и живёт недолго — обменивайте его сразу.
| Параметр | Обязателен | Что указывать |
|---|---|---|
| grant_type | да | authorization_code |
| code | да | Значение code из возврата. |
| redirect_uri | да | Тот же адрес, что в запросе авторизации. |
| code_verifier | да | Исходная строка, из которой посчитан code_challenge. |
| client_id | для none | Ваш client_id. |
| resource | нет | https://api.coremind.ru/v1, как в запросе авторизации. |
| client_assertion_type | для private_key_jwt | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
| client_assertion | для private_key_jwt | Одноразовый JWT, подписанный ключом из jwks клиента: iss и sub — ваш client_id, aud — адрес этого маршрута или https://api.coremind.ru, обязательны exp и уникальный jti. Вместо client_id. |
curl -s https://api.coremind.ru/oauth/token \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=<redirect_uri> \
-d client_id=<client_id> \
-d code_verifier=<code_verifier>Ответ:
| Поле | Что содержит |
|---|---|
| access_token | Токен для /v1. Непрозрачная строка — храните как есть, не разбирайте. |
| token_type | Bearer |
| expires_in | Срок жизни access-токена в секундах. |
| refresh_token | Для обновления. Есть, если у клиента грант refresh_token. |
| scope | Области, которые пользователь действительно выдал, через пробел. |
{
"access_token": "<access_token>",
"token_type": "Bearer",
"expires_in": <seconds>,
"refresh_token": "<refresh_token>",
"scope": "me:read generations:read"
}Ошибки приходят в формате OAuth — error и error_description: invalid_grant, если код истёк, уже использован или не совпали redirect_uri либо code_verifier; invalid_client, если клиент не найден или подпись не прошла.
Обновление токена
Access-токен короткий — истёк, обновите его тем же POST /oauth/token:
| Параметр | Обязателен | Что указывать |
|---|---|---|
| grant_type | да | refresh_token |
| refresh_token | да | Последний полученный refresh-токен. |
| client_id | для none | Ваш client_id. |
| scope | нет | Сузить набор областей нового access-токена — только из уже выданных. |
| client_assertion_type | для private_key_jwt | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
| client_assertion | для private_key_jwt | Одноразовый JWT, подписанный ключом из jwks клиента: iss и sub — ваш client_id, aud — адрес этого маршрута или https://api.coremind.ru, обязательны exp и уникальный jti. Вместо client_id. |
curl -s https://api.coremind.ru/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=<refresh_token> \
-d client_id=<client_id>Ответ — те же поля, что при обмене кода.
- Refresh-токен ротируется при каждом использовании: ответ несёт новый, старый больше не принимается. Сохраняйте новую пару атомарно, до первого вызова с ней.
- Повторное предъявление уже использованного refresh-токена отзывает всё разрешение со всеми токенами. Исключение — повтор сразу после обмена: когда два процесса обновляют один токен параллельно, проигравший получает
invalid_grant, а разрешение остаётся. Перечитайте сохранённую пару и повторите запрос с новым токеном. - Неиспользуемый refresh-токен со временем истекает, а у самого разрешения есть абсолютный срок, который ротация не продлевает. После любого из них пользователя проводят через авторизацию заново.
Вызов API
Access-токен передаётся так же, как ключ, — в заголовке Authorization: Bearer, и только в нём. Он действует только на /v1 и только в пределах выданных областей. Из браузера на /v1 его не отправить: CORS не пропускает заголовок Authorization.
curl -s https://api.coremind.ru/v1/whoami \
-H "Authorization: Bearer <access_token>"{
"user_id": "<user_id>",
"role": "<role>",
"channel": "oauth",
"key_id": "<grant_id>",
"scopes": ["me:read", "generations:read"]
}channel: "oauth" подтверждает, что запрос прошёл по токену, key_id — идентификатор разрешения, scopes — выданные области.
| Код | HTTP | Что делать |
|---|---|---|
| unauthorized | 401 | Заголовок Authorization не передан. |
| invalid_token | 401 | Токен неизвестен, истёк, выпущен для другого ресурса или разрешение отозвано. Обновите токен; отказал и refresh — нужна новая авторизация. |
| insufficient_scope | 403 | Не хватает области; WWW-Authenticate называет её в scope. Запросите её новой авторизацией. |
| api_access_not_granted | 403 | У аккаунта нет доступа к API. |
| account_suspended | 403 | Аккаунт заблокирован. |
Остальные коды — на странице кодов ошибок.
Отзыв доступа
Пользователь видит подключённые приложения в профиле, в разделе «Доступ приложений», и может отозвать любое. Отзыв действует сразу: разрешение и все его токены перестают приниматься со следующего запроса, приложение получает invalid_token.
Приложение может отказаться от своих токенов само — POST /oauth/revoke по RFC 7009. Разрешение при этом остаётся: токены, выданные после повторной авторизации, не затрагиваются. Разрешение целиком отзывается, когда это делает пользователь в профиле или когда уже использованный refresh-токен предъявлен снова — на отзыв всегда, на обмен — кроме повтора сразу после обмена (см. выше).
| Параметр | Обязателен | Что указывать |
|---|---|---|
| token | да | Отзывается только предъявленный токен — остальные токены разрешения продолжают работать. Чтобы отказаться от доступа, отзовите каждый выданный вам токен, который ещё хранится, — refresh- и access-токены от всех авторизаций. Полностью и сразу доступ закрывает только отзыв разрешения пользователем в профиле. |
| token_type_hint | нет | refresh_token или access_token. |
| client_id | для none | Ваш client_id. |
| client_assertion_type | для private_key_jwt | urn:ietf:params:oauth:client-assertion-type:jwt-bearer |
| client_assertion | для private_key_jwt | Одноразовый JWT, подписанный ключом из jwks клиента: iss и sub — ваш client_id, aud — адрес этого маршрута или https://api.coremind.ru, обязательны exp и уникальный jti. Вместо client_id. |
curl -s https://api.coremind.ru/oauth/revoke \
-d token=<refresh_token> \
-d token_type_hint=refresh_token \
-d client_id=<client_id>Ответ — 200 с пустым телом, в том числе для уже отозванного или неизвестного токена.