Overwatch Tournaments нужны функциональные cookie, чтобы сохранять вход и язык интерфейса. Аналитические cookie необязательны и только показывают, как используются страницы, — подробнее в политике конфиденциальности.

Перейти к содержимому

Аутентификация и ключи API

Две учётки ходят в API через один и тот же заголовок Authorization: Bearer: сессионный токен браузера и API-ключ. Здесь — чем они отличаются, как их получить, что ключ может и чего не может, и какие лимиты вы увидите.

Логин и игрок — разные сущности

Логин (auth.user) — это учётная запись: пароль, сессии, OAuth-подключения, API-ключи, роли и членство в воркспейсах. Игрок (players.user) — это профиль в турнирных данных: BattleTag, ранги, история встреч, достижения. Игрок существует и без логина: его создают импорт и разбор логов. Связь односторонняя и не шире, чем один к одному — у игрока есть поле auth_user_id, и логин может быть привязан максимум к одному игроку. Подробнее о слоях — в статье Модель данных.

Для API это значит: идентификаторы из /api/v1/auth/me и из /api/v1/users/... — из разных пространств, и сопоставлять их можно только через привязку.

Как аутентифицируется браузер

Сессия начинается с OAuth. Провайдеры — Discord, Twitch и Battle.net; включённым считается тот, у которого в деплое заданы client_id, client_secret и общий redirect_uri. Список доступных отдаёт публичный эндпоинт:

GET /api/v1/auth/providers HTTP/1.1
Host: owt.craazzzyyfoxx.me

Дальше браузер получает ссылку авторизации (GET /api/v1/auth/oauth/{provider}/url), уходит к провайдеру и возвращается с кодом, который сайт обменивает на пару токенов (POST /api/v1/auth/oauth/{provider}/callback). Обращение к отключённому провайдеру — 404.

Есть и вход по паролю — POST /api/v1/auth/login с телом {"email": ..., "password": ...}. Он работает только для аккаунтов, у которых пароль вообще задан: аккаунт, созданный через OAuth, пароля не имеет, пока владелец не поставит его через POST /api/v1/auth/set-password. Неверная пара и аккаунт без пароля неразличимы снаружи — оба дают 401.

Оба пути возвращают одну и ту же пару:

{
  "access_token": "<JWT>",
  "refresh_token": "<opaque>",
  "token_type": "bearer"
}

Время жизни задано константами сервиса: access-токен — 15 минут, refresh-токен — 30 суток.

Обновление сессии

POST /api/v1/auth/refresh с телом {"refresh_token": ...} выдаёт новую пару и отзывает предъявленный refresh-токен: в сессии всегда живёт ровно один. Идентификатор сессии (sid) при этом сохраняется, поэтому список сессий и их отзыв продолжают указывать на ту же запись.

Ротация прощает потерянный ответ: если клиент повторяет токен, который был обновлён меньше 60 секунд назад, обмен проходит ещё раз. Позже это считается повторным использованием — запрос получает 401, и отзывается та сессия, к которой относился токен (остальные сессии аккаунта живы).

Отзыв сессии закрывает и ещё не истёкший access-токен: его sid попадает в чёрный список, который проверяется при каждой валидации. Задержка — только кеш вердикта на шлюзе, 30 секунд для сессионного токена.

Как токен едет в запросе

REST-маршруты читают учётку только из заголовка Authorization. Куки — это способ хранения на стороне сайта: фронтенд кладёт access-токен в куку owt_access_token, а refresh-токен — в httpOnly-куку owt_refresh_token, и сам подставляет access-токен в заголовок при каждом вызове API. Единственное исключение — скачивание лога матча (GET /api/v1/matches/{match_id}/log): по этой ссылке браузер переходит сам, JavaScript заголовок повесить не может, поэтому маршрут принимает и сессионную куку. Заголовок и там имеет приоритет.

Параметр ?token= в REST не читается вообще: он применяется только к рукопожатию WebSocket — см. статью Realtime (WebSocket).

Ключи API

Ключ создаётся в админке: Admin → Access → API keys → Create key. Форма спрашивает имя, воркспейс, набор скоупов и необязательную дату истечения; выпускать ключи в воркспейсе может тот, у кого есть право team.create — в самом воркспейсе или глобально, — а также суперпользователь. Страница ходит в собственный эндпоинт сайта /bff/account/api-keys, который авторизуется сессионной кукой и уже от неё вызывает POST /api/v1/auth/api-keys — поэтому ключом нельзя выпустить другой ключ.

Ключ выглядит так:

owt_sk_<public_id>_<secret>

public_id — 16 hex-символов, secret — 64. В базе хранится только хеш секрета, а полная строка возвращается ровно один раз, в ответе на создание, и больше нигде: страница прямо предупреждает, что повторно её не покажет. Ключи, выпущенные до переименования проекта, начинаются с aqt_sk_ и продолжают проходить проверку.

Свойства ключа:

  • Один воркспейс. Он задаётся при создании и не меняется. Во второй воркспейс ключ не попадёт никак.
  • Скоупы — это имена прав RBAC из общего каталога: team.create, registration.approve, подстановочное admin.*. Никакой отдельной системы «скоупов API» нет, поэтому эндпоинты проверяют ключ той же проверкой прав, что и человека.
  • Пересечение с правами владельца. При каждой валидации ключа берутся его скоупы и отфильтровываются те, которых у владельца в этом воркспейсе сейчас нет. Выдать ключу больше, чем есть у вас, не даст и сама форма создания — попытка возвращает 403.
  • Никакого глобального доступа. В полезной нагрузке ключа нет ролей, нет глобальных прав и никогда нет признака суперпользователя. Запреты (denies) владельца, наоборот, переносятся: запрет сильнее любого разрешения.
  • Ключ не переживает владельца. Если владелец потерял членство в воркспейсе, деактивирован, или воркспейс выключен — ключ перестаёт валидироваться, без отдельного отзыва.
  • Ключ без скоупов аутентифицируется, но не проходит ни одной проверки прав: в списке ключей такой помечен как inert.

Отправляется ключ так же, как сессионный токен:

curl -H "Authorization: Bearer owt_sk_a1b2c3d4e5f60718_…" \
  "https://owt.craazzzyyfoxx.me/api/v1/tournaments?workspace_id=1"

Ключ описывает сам себя:

GET /api/v1/auth/api-keys/self HTTP/1.1
Host: owt.craazzzyyfoxx.me
Authorization: Bearer owt_sk_a1b2c3d4e5f60718_…
{
  "id": 12,
  "name": "overlay-bot",
  "workspace_id": 1,
  "public_id": "a1b2c3d4e5f60718",
  "owner_id": 4,
  "owner_username": "craazzzyyfoxx",
  "scopes": ["registration.read"],
  "expires_at": null,
  "revoked_at": null,
  "last_used_at": "2026-09-23T10:15:00Z",
  "created_at": "2026-09-01T12:00:00Z",
  "updated_at": null
}

Отзыв (DELETE /api/v1/auth/api-keys/{id}) и истечение expires_at действуют не мгновенно: шлюз кеширует вердикт валидации ключа 5 секунд (для сессионного токена — 30 секунд).

Что принимает только сессия, а что только ключ

Всё под /api/v1/auth/ — операции над собственным аккаунтом, сессиями и учётными данными, поэтому их разрешено выполнять только сессионным токеном. API-ключ там получает 401.

МаршрутСессияКлюч
GET /api/v1/auth/meдада
GET /api/v1/auth/api-keys/self, .../self/quotaнет (403)да
/api/v1/auth/logout, /logout-all, /sessions, /set-password, DELETE /meданет (401)
Создание, переименование, отзыв ключей и правка их квотданет (401)
Доменные маршруты (/api/v1/tournaments, /registration, ...)дада, в пределах скоупов

Смысл ровно один: ключом нельзя выпустить другой ключ, продлить сессию или тронуть аккаунт.

Квоты

Квоты считаются в трёх областях (scope), и каждая проверяется отдельно:

ScopeКого ограничивает
workspaceвсё сообщество целиком: все его ключи и все участники
keyодин API-ключ
sessionодну сессию вошедшего участника

Измерений пять, из них три — счётчики:

  • requests_per_minute — учитываемых вызовов в минуту, окно 60 секунд.
  • heavy_per_day — «тяжёлых единиц» в сутки; дорогая операция списывает свою стоимость, окно сбрасывается в 00:00 UTC.
  • concurrent_heavy — сколько тяжёлых задач держится одновременно; завершённая или упавшая освобождает слот.

И два — потолки одного запроса: max_upload_bytes (размер тела) и max_items_per_request (количество элементов в пачке).

Значения берутся с трёх уровней: тариф воркспейса, переопределение воркспейса, переопределение ключа. Ближайший заданный уровень выигрывает для счётчиков, а для потолков запроса побеждает самое узкое значение. Пустое значение означает «наследовать», а не «ноль».

Тяжёлыми считаются именно помеченные операции — расчёт баланса команд, импорт и экспорт составов, загрузка и разбор логов, пересчёт достижений, обучение и инференс аналитики, синхронизация с внешними таблицами и Challonge. Обычные чтения тратят только requests_per_minute.

Что видит клиент

Превышение любого счётчика — это 429 с заголовком Retry-After в секундах и телом, где code равен rate_limited, а детали лежат в fields:

{
  "fields": [
    {
      "field": null,
      "msg": "quota exceeded",
      "code": "quota_exceeded",
      "limit_name": "heavy_per_day",
      "scope": "workspace",
      "limit": 50
    }
  ],
  "retry_after": 1800,
  "detail": "quota exceeded",
  "code": "rate_limited"
}

limit_name — какое измерение упёрлось, scope — чья это квота. Для concurrent_heavy окна нет, поэтому Retry-After там всегда 30 секунд. Потолки одного запроса проверяются до того, как тело будет разобрано, и бюджет на них не тратится: слишком большое тело — 413 с code payload_too_large и quota_payload_too_large в fields, слишком большая пачка — 400 с code bad_request и quota_items_too_many в fields. В v2 те же данные лежат в error.details — см. статью HTTP API.

Своё потребление ключ смотрит сам: GET /api/v1/auth/api-keys/self/quota возвращает по каждой применимой области потолок, потраченное и время до сброса. Незаданное измерение приходит как null — «без ограничений», при этом счётчик всё равно показан.

Квоты считаются в общем счётчике, а шлюз ещё и меряет каждый ключ по его собственному requests_per_minute на входе. Это один и тот же бюджет, а не два: обе стороны пишут в один счётчик.

401 против 403

  • 401 — учётка не предъявлена, не распознана или больше не действует: нет заголовка Authorization на маршруте, который его требует; истёкший, отозванный или испорченный токен; ключ с неверным секретом, отозванный, просроченный, с выключенным владельцем или воркспейсом; API-ключ на маршруте, где нужна сессия. Различить причины снаружи нельзя — это сделано намеренно.
  • 403 — учётка распознана, но действие не разрешено: не хватает права в воркспейсе, ключу не выдан нужный скоуп, у владельца стоит запрет, или вы просите сессионным токеном то, что отвечает только ключу.

Отдельный случай — 503: шлюз не смог получить вердикт от сервиса аутентификации (перегрузка, обрыв, таймаут). Это не «вас разлогинили»; ответ несёт Retry-After: 1, и правильная реакция — повторить, а не выбрасывать сессию.

См. также