OAuth для приложений

OAuth 2.1 нужен приложению, которое работает с API от имени другого человека: MCP-клиенту, редактору, интеграции для чужих аккаунтов. Пользователь нажимает «Разрешить доступ» на экране согласия, и приложение получает токен на его аккаунт, не узнавая ни пароля, ни ключа. Для своих скриптов и своего сервера проще API-ключ.

Поддерживается один поток — код авторизации с PKCE и refresh-токеном. Сервер авторизации и API живут на https://api.coremind.ru, маршруты сервера авторизации — под /oauth/….

Как подключиться

  1. Прочитайте метаданные сервера авторизации и ресурса.
  2. Получите client_id — опубликуйте документ с метаданными клиента или зарегистрируйтесь через /oauth/register.
  3. Отправьте пользователя на авторизацию с PKCE.
  4. Обменяйте код на access- и refresh-токен.
  5. Вызывайте /v1 с access-токеном.
  6. Обновляйте токен по истечении и отзывайте доступ, когда он больше не нужен.

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", а также клиент, у которого все адреса возврата — loopback http: (он считается 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_typeresponse_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_jwturn: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_typeBearer
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_jwturn: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Что делать
unauthorized401Заголовок Authorization не передан.
invalid_token401Токен неизвестен, истёк, выпущен для другого ресурса или разрешение отозвано. Обновите токен; отказал и refresh — нужна новая авторизация.
insufficient_scope403Не хватает области; WWW-Authenticate называет её в scope. Запросите её новой авторизацией.
api_access_not_granted403У аккаунта нет доступа к API.
account_suspended403Аккаунт заблокирован.

Остальные коды — на странице кодов ошибок.

Отзыв доступа

Пользователь видит подключённые приложения в профиле, в разделе «Доступ приложений», и может отозвать любое. Отзыв действует сразу: разрешение и все его токены перестают приниматься со следующего запроса, приложение получает 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_jwturn: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 с пустым телом, в том числе для уже отозванного или неизвестного токена.