Refresh — httpOnly cookie
→ CSRF
httpOnly
Secure
SameSite=Strict
Path=/api/v1/auth
При реализации — префикс имени
__Secure-
(defense-in-depth: браузер примет cookie только при флаге
Secure).
Path —
/api/v1/auth, не точечный
/refresh: cookie нужна и logout — по ней
завершение сессии
опознаёт refresh-токен.
Чекбокс управляет временем жизни cookie, не токена:
-
Снят → session cookie (без Max-Age), удаляется при
закрытии браузера (общие машины).
-
Установлен → persistent cookie с Max-Age = TTL
refresh-токена (30 д).
Refresh-токен в БД в обоих случаях действует по своему TTL.
Refresh token rotation
При каждом refresh — новый токен, старый инвалидируется.
Grace-период ~10 с: только что ротированный токен в этом
окне принимается повторно и возвращает тот же новый —
доброкачественная гонка вкладок безвредна. Предъявление старого
токена за пределами окна — reuse detection → аннулировать всю
цепочку (family), не все сессии пользователя.
Access token в SPA
Хранится в памяти JS (не localStorage) — защита от XSS.
Передача — Authorization: Bearer. При перезагрузке
восстанавливается через /refresh.
Refresh queue — concurrent 401
Один общий /refresh на все запросы, упёршиеся в
истёкший токен. Interceptor держит Promise текущего refresh
(или null):
- 401, а refresh уже идёт → ждём тот же Promise.
- 401, refresh не идёт → запускаем его, сохраняем Promise.
- Успех → все ждавшие повторяют запрос с новым токеном.
- Провал → всех отклоняем, logout.
Защиты:
-
Повтор ровно один раз — флаг
_retry на запросе.
401 на уже повторённом → сразу logout, без нового круга
refresh (защита от зацикливания).
-
Refresh запускает только 401 по истёкшему access token. 401 от
самого
/refresh → refresh-токен мёртв → logout без
рекурсии; 401 по правам пропускается без refresh.
-
Таймаут на Promise refresh — провисший запрос отклоняет всех
ждущих и ведёт в logout, а не держит их бесконечно.
-
Между вкладками одноразовый refresh-токен может уйти дважды.
Гонку гасит grace-период ротации
↑ Refresh token rotation: опоздавшая вкладка получает тот же новый токен и
продолжает работу.
Параллельные сессии
Сессия = одна
token family
— один вход с устройства или браузера; активная сессия —
family с живым refresh-токеном. Число параллельных сессий
не ограничено; видимость и управление зависят от роли:
-
Пользователь — свои сессии, самозащита. Видит все
свои сессии и завершает любую, кроме текущей, — по одной или
«все остальные» (защита при компрометации). Карточка несёт
устройство, IP и последнюю активность (из
refresh-токена).
Экран —
управление сессиями.
-
Админ — чужие сессии, без приватных деталей.
Устройство, IP и гео владельца админу не показываются (это
приватные данные, для реагирования не нужны): в
карточке пользователя
только счётчик активных сессий и грубое «завершить все».
Оно удаляет все refresh-токены — новые access не выдаются,
уже выданный stateless access-JWT доживает своё 15-минутное
окно (↑ JWT claims).
Когда подозрительная сессия известна, а владелец нет, —
пользователя называет
audit log
(auth-события несут session id), дальше — та же карточка.
что добавит v2
Лимит сессий per-user (default 5, настраивается в
Admin Panel;
при превышении самая старая завершается автоматически).
Оповещение о входе с нового устройства / гео — баннер
в UI плюс письмо.
Мгновенный отзыв access-токена через jti-blacklist в
Redis — без ожидания 15-минутного окна.
Инвалидация при деактивацииv2
Отзыв refresh tokens + jti access token в Redis
blacklist (TTL = lifetime access token).
-
Критичные действия (смена пароля, управление пользователями,
настройки безопасности, завершение сессий) требуют повторного
ввода пароля — даже в рамках открытой сессии.
-
С включённым MFA проверка принимает TOTP-код.
-
После успеха — grace-period (несколько минут, привязан к
текущей сессии): соседние критичные действия проходят без
повторного запроса, затем проверка снова закрывается.
-
Неверный ввод считается под тем же rate-limit, что и вход.
Стандартный: удалить refresh token из БД (опознан по
refresh-cookie) + очистить httpOnly cookie. Access token
истекает сам (15 мин). Кнопка «Выйти» —
хедер админки.
Все устройства: удалить все refresh tokens
пользователя — сам пользователь, из UI управления сессиями.
Принудительный: то же «все устройства», но админом —
из
карточки пользователя.
Удаляются все refresh-токены пользователя; новые access не
выдаются, уже выданный stateless access-JWT доживает своё
≤15-минутное окно.