Аутентификация и ключи 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, и правильная реакция — повторить, а не выбрасывать сессию.
См. также
- HTTP API — версии, конверт ответа, коды ошибок, кэш и лимиты на границе.
- Воркспейсы и права — из чего складывается RBAC, который пересекается со скоупами ключа.
- Realtime (WebSocket) — как та же пара учёток работает в сокете.
- Справочник эндпоинтов — полный список маршрутов v1 и v2.