JWT и сервисная авторизация
VideoGrace использует bearer-token модель для всех runtime-каналов. CommandLoop выполняет первичную авторизацию, а затем тот же access_token используется для BlobChannel, WSMedia и REST API.
Runtime flow
sequenceDiagram
participant Client
participant Core as VideoGrace Server
participant DB
participant Media as Blob/WSMedia/API
Client->>Core: connect_request(login/password)
Core->>DB: load user + grants
Core-->>Client: connect_response(access_token, bootstrap refresh_token)
Client->>Core: POST auth_refresh(login, cookie_slot, bootstrap token)
Core-->>Client: access_token + Set-Cookie(HttpOnly refresh) + rotated fallback
Client->>Media: Authorization/connect_request access_token
Media->>Core: validate token
Core-->>Media: client_id + grants
Client->>Core: POST /api/v1.0?auth_refresh + HttpOnly cookie (preferred)
Core->>DB: validate refresh token hash
Core-->>Client: new access_token + rotated Set-Cookie
Client->>Core: POST /api/v1.0?auth_logout
Core->>DB: revoke access jti + refresh hash
access_token выпускается как JWT HS256. Штатный установщик создаёт и
сохраняет [Auth] JwtSecret, если не задан VG_JWT_SECRET. Сам сервер secret не
генерирует: для ручной установки отсутствие настройки остаётся явной ошибкой.
Legacy opaque token остаётся только переходным механизмом для уже открытых
внутренних каналов; новый код не должен зависеть от него.
Access JWT
JWT содержит только данные, нужные для быстрой авторизации:
| Claim | Значение |
|---|---|
iss |
VideoGrace. |
aud |
videograce. |
typ |
access. |
sub |
client_id. |
login |
Логин пользователя. |
name |
Отображаемое имя. |
grants |
Битовая маска прав. |
scope |
Каналы и API scopes. |
jti |
ID токена для точечного revoke. |
iat / exp |
Время выпуска и истечения. |
Проверка JWT включает:
- разбор трех частей JWT;
- проверку
alg=HS256; - HMAC-SHA256 подпись через серверный secret;
- проверку
iss,aud,typ, обязательныхsub,iat,exp,jti,scope; - проверку времени выпуска и истечения;
- проверку требуемого scope для конкретного канала;
- проверку
jtiпо таблицеjwt_revoked; - загрузку актуального пользователя из БД только по положительному
sub.
Клиентам не нужно читать claims. Для клиента token остается непрозрачной строкой.
Refresh token
refresh_token - случайный одноразовый секрет, не JWT. Сервер хранит в БД только HMAC-хеш refresh token и TTL. Это позволяет:
- не хранить access JWT в БД;
- продлевать пользовательскую сессию без повторного пароля;
- ротировать refresh token при каждом успешном обновлении;
- отозвать refresh token без хранения его открытого значения;
- не раскрывать пароль сервисам и браузерным каналам.
При первичном входе по паролю WebSocket возвращает bootstrap refresh_token. Web-client немедленно обменивает его через same-origin HTTP endpoint на отдельную для аккаунта cookie vg_refresh_<cookie_slot> с атрибутами Secure; HttpOnly; SameSite=Strict. Cookie является основным хранилищем сессии.
Последующие обновления используют cookie, которую JavaScript прочитать не может:
POST /api/v1.0?auth_refresh
Content-Type: application/json
{
"login": "alice",
"cookie_slot": "stable-non-secret-account-slot",
"include_refresh_fallback": true
}
Ответ:
{
"result": 200,
"access_token": "...",
"refresh_token": "rotated-revocable-fallback",
"refresh_cookie": true,
"expires_in": 604800,
"token_type": "Bearer"
}
Успешный запрос атомарно отзывает старый refresh token и выпускает новый. По умолчанию он возвращается только в Set-Cookie. Web-client передает include_refresh_fallback=true, поэтому тот же новый token также возвращается в JSON и сохраняется как резерв для Android WebView и других профилей, которые теряют cookie между запусками. При восстановлении клиент сначала пробует HttpOnly cookie и использует fallback только после 401; успешный fallback одновременно заново устанавливает cookie и ротирует token. Повторное использование старого token получает 401. cookie_slot не является секретом: он позволяет держать несколько аккаунтов одного сервера в одном браузере, не смешивая их cookie.
Cookie-auth endpoints принимаются только с того же origin, что и сервер. Credentialed CORS для них намеренно запрещен.
Logout
POST /api/v1.0?auth_logout
Authorization: Bearer <access_token>
Content-Type: application/json
{
"login": "alice",
"cookie_slot": "stable-non-secret-account-slot"
}
Сервер отзывает jti access JWT и refresh token cookie одной сессии, затем очищает cookie через Max-Age=0. Если access JWT уже истек, достаточно действующей cookie.
Scope enforcement
| Scope | Где проверяется |
|---|---|
control |
Вход в CommandLoop. |
media |
Вход в WSMedia. |
blob |
BlobChannel и storage blob/upload API. |
api |
Основной REST API. |
integration:rooms |
Partner Integration API комнат. |
Обычный пользовательский token получает control, media, blob, api. Service token получает scopes из service_accounts; отсутствие нужного scope не компенсируется уже открытой control-сессией.
Для CAN service accounts прикладные scopes разворачиваются в транспортные: conference:join и chat:write требуют control, media:render требует control + media, storage:write требует api + blob.
Партнерскому Integration API account для скачивания записей из recording.ready
нужны integration:rooms + blob. Scope blob разрешает обращение к storage API,
но не открывает чужие файлы: отдельно проверяется ACL записи и владелец комнаты.
После изменения scopes надо получить новый access token; старые JWT не меняются.
Browser session
Web-client использует пароль только для первичного обмена на token pair. После успешного входа:
localStorage.vg_sessions_v2содержит компактные несекретные метаданные аккаунтов и отметку о выданной server-side cookie;cookie_slotдетерминированно вычисляется изserver + loginи не сохраняется;sessionStorage.vg_session_tokens_v1содержит короткий access token и текущую копию refresh token для активной вкладки;localStorage.vg_refresh_tokens_v1содержит только ротируемый и отзываемый refresh fallback по ключу аккаунта; пароль и access token туда не записываются;sessionStorage.vg_active_session_v2выбирает активный аккаунт вкладки;- access token дублируется в session-cookie
tдля защищенных blob/media URL, а основной постоянный refresh хранится вSecure; HttpOnlycookie; - F5 использует текущий access token, а новая вкладка/PWA получает новый access token по refresh-cookie либо по fallback, если браузер потерял cookie;
- несколько аккаунтов одного origin используют разные refresh-cookie;
- старые поля
pass,password,accessToken,refreshTokenудаляются изlocalStorageмиграцией.
Запись реестра обрабатывает QuotaExceededError: UI не падает при исчерпании общей квоты origin, а текущая вкладка временно использует in-memory копию. История сообщений при этом остается в IndexedDB и не очищается.
Fallback сознательно меняет security trade-off: XSS может прочитать его из localStorage, но не получает пароль, а token ограничен сроком, хранится на сервере только как HMAC-хеш, ротируется после использования и отзывается при logout. Без fallback некоторые Android-устройства требовали пароль при каждом запуске. При удалении аккаунта сервер отвечает AccountNotFound; web-client удаляет соответствующие локальные метаданные, fallback и IndexedDB. Если пользователь пришел по invite и других аккаунтов нет, после очистки запускается гостевая авторегистрация.
Service accounts
Сервисные процессы не должны хранить пользовательские пароли. Для production используется таблица service_accounts: она связывает имя сервиса с обычным client_id, grants которого уже настроены в VideoGrace.
Типовой пример:
insert into service_accounts
(name, client_id, scopes, enabled, created_at)
values
('transcriber', 228800001, '["conference:join","media:render","chat:write"]', 1, strftime('%s','now'));
Когда администратор отправляет CAN job transcriber.start без payload.auth, core выпускает короткий access JWT для service account и добавляет его в job payload. Worker получает задание уже с bearer token и подключается к CommandLoop стандартным путем.
Инварианты безопасности
- JWT-secret должен быть серверным секретом и не должен попадать в клиентский bundle.
- Refresh token нельзя передавать в media/blob/CAN worker как access token.
- Основной browser refresh token должен находиться в
Secure; HttpOnly; SameSite=Strictcookie; JS fallback допустим только для web-client, должен ротироваться и удаляться при logout. - Пароль пользователя нельзя сохранять в browser storage или повторять при reconnect.
- CAN service token авторизует подключение worker'а к
/can, но не дает пользовательских grants. - Service account должен быть отдельным пользователем с минимально нужными правами.
- Нельзя писать access/refresh token в application logs.
- Для точечного отзыва access JWT используйте
jtiи таблицуjwt_revoked; для полного сброса сессий меняйтеJwtSecret.