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

POST /api/v1/auth/sign-out — Выход (отзыв refresh-сессии)

Отзыв текущей refresh-сессии (Active → Revoked, RevokedReason = UserLogout) и очистка refresh-cookie. Опция allDevices = true отзывает все активные сессии пользователя (UserRevokedAll).

Назначение

Пользователь завершает сессию на устройстве или разом на всех устройствах (например, при утере телефона). Витрина списка сессий — контекст user-activity (ADR-0054), source of truth и отзыв — здесь. Owning-контекст — identity.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupauth
Статусapproved
Документdocs/06-services/identity/api/investor/sign-out.md

Актор и доступ

ПроверкаЗначение
Actor typeUser (валидный access-JWT; при истёкшем access выход выполняется без сервера — клиент чистит хранилище)
Auth policy / Permissionsauthenticated; permission не требуется (self-операция; каталожное право витрины — user-activity.sessions.revoke-self)
Scope (RBAC)не применяется
Record-levelотзываются только сессии UserId = actor (claim sub); sid claim определяет «текущую»
Tenant isolationне применяется — RefreshSession не tenanted, record-level по UserId
AuditRevokedByUserId = actor, событие SessionRevokedEvent на каждую отозванную

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/v1/auth/sign-out
Success status204
Idempotency headerне применяется — повтор по уже отозванной сессии возвращает 204 (идемпотентный исход by design)
Correlationсквозной trace через traceparent

Входные данные / параметры запроса

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
UserIdidentity claims (sub)GuidДаRequiredrecord-level
SessionIdidentity claims (sid)Guid?Нет (нет у токенов без сессии)связь access ↔ session (user-session.md)
AllDevicesbodyboolНетdefault false

Модель ответа SignOutCommandResult

HTTP-тело отсутствует (204 No Content); внутренняя модель для логов:

ПолеТипОбязательностьИсточникMaskingОписание
RevokedCountintДачисло затронутых сессий0 — валидный исход (токен без sid / сессия уже отозвана).

Алгоритм

  1. Контекст и доступ. Актор — аутентифицированный пользователь; известны UserId (sub) и, опционально, SessionId (sid). Требуется только аутентификация.
  2. Проверка. Существование сессии не проверяется как ошибка: отсутствие/уже-отозванность → RevokedCount = 0, всё равно 204 (выход обязан удаваться всегда).
  3. Правила и переходы. Переход Active → Revoked (stateDiagram user-session.md) для набора сессий:
    • AllDevices = false: отзывается активная сессия UserId = actor, Id = SessionId, RevokedReason = UserLogout; при отсутствии SessionId — изменений нет, RevokedCount = 0;
    • AllDevices = true: отзываются все активные сессии UserId = actor, RevokedReason = UserRevokedAll (включая текущую). Маппинг: Status = Revoked, RevokedAt = now, RevokedByUserId = actor, UpdatedAt = now.
  4. Что меняется. Пакетный (set-based) отзыв в одном согласованном изменении, без конфликтов (терминальное состояние; гонка с параллельным refresh отсекается условием Status == Active).
  5. События. Доменное SessionRevokedEvent (SessionId, UserId, RevokedReason) — по одному на отозванную сессию. Интеграционное событие не публикуется: витрина user-activity подписана на доменную ленту через internal-механизм; публикация в pin.identity.sign-ins.v1 — только про входы (user-session.md «События»).
  6. Результат. 204; web — Set-Cookie: pin:refresh=; Max-Age=0 (очистка). Access-JWT остаётся криптографически валиден до exp (≤ 15 мин) — привилегированные операции дополнительно гейтятся check-permission (deny по отозванной сессии не проверяется — деградация принята и ограничена коротким TTL access-токена).

Диаграмма

Побочные эффекты

ТипДетали
Интеграционные событияnone (см. шаг 5)
Внешние вызовыnone
Проекции / поискnone (permissions не меняются — кэш perm:{userId} не трогается)
Уведомленияnone
Окно согласованности

Витрина устройств user-activity (ADR-0054) подписана на доменную ленту сессий и снимает отозванные записи асинхронно (лаг — секунды): пользователь сразу теряет refresh (cookie очищена), а в списке устройств отозванная сессия исчезает с задержкой. Уже выданный access-JWT остаётся валиден до exp (≤ 15 мин) — привилегированные операции доотсекаются check-permission; на неприв. read-операциях (напр. GET /me) отзыв не проверяется (принятая деградация, окно ≤ TTL access).

Предусловия и постусловия

  • Пред: предъявлен access-JWT (валидный или уже истёкший — во втором случае клиент выходит локально без сервера). Известны UserId (sub) и, опционально, SessionId (sid).
  • Пост (AllDevices = false): активная сессия Id = SessionIdRevoked (UserLogout); при отсутствии sid изменений нет (RevokedCount = 0). Cookie refresh очищена.
  • Пост (AllDevices = true): все активные сессии UserId = actorRevoked (UserRevokedAll), включая текущую; на каждую — SessionRevokedEvent.

Acceptance Criteria

  • Given аутентифицированный пользователь с активной текущей сессией, When POST /sign-out (allDevices = false), Then 204, текущая сессия Active → Revoked (UserLogout), RevokedCount = 1, cookie pin:refresh очищена, публикуется SessionRevokedEvent.
  • Given пользователь с 3 активными сессиями, When POST /sign-out (allDevices = true), Then 204, все 3 → Revoked (UserRevokedAll), RevokedCount = 3, 3× SessionRevokedEvent.
  • Given access-JWT без claim sid, When POST /sign-out (allDevices = false), Then 204, RevokedCount = 0 (no-op — валидный исход).
  • Given сессия уже отозвана, When повторный POST /sign-out, Then 204, RevokedCount = 0 (идемпотентный исход, событие повторно не публикуется).
  • Given нет/просрочен access-JWT, When POST /sign-out, Then 401; клиент всё равно очищает локальное хранилище (эквивалент выхода).

Ошибки и failure modes

КодHTTP statusУсловиеСообщениеRetry
UNAUTHORIZED401Нет/просрочен access-JWTклиент чистит локально (эквивалент выхода)
INTERNAL500Инфраструктурный сбойgenericretry; клиент всё равно чистит локальное хранилище
Бизнес-ошибок нет

Выход идемпотентен и «всегда успешен» by design.

Совместимость и наблюдаемость

  • Токены без sid дают no-op 204 — клиент может мигрировать раньше бэкенда.
  • Logs: UserId, RevokedCount, AllDevices; metrics: identity_sign_out_total{all_devices}; traces: OTel; dashboard identity-overview; runbook: массовый отзыв сессий (README identity).

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные документы

  • Сущность: domain/user-session.md (stateDiagram, SessionRevokeReason).
  • Вход/ротация: refresh-token.md; витрина сессий: docs/06-services/user-activity/ (ADR-0054).