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

UC-IDN-003. Включение двухфакторной аутентификации

Назначение

Пользователь включает двухфакторную аутентификацию (2FA) в личном кабинете: запрашивает код, подтверждает его, после чего вход по паролю/пин-коду дополнительно требует СМС-код. Вызывает любой аутентифицированный владелец учётки. Выключение — зеркальный поток, зафиксирован альтернативой A3. Связан с контекстом identity.

Metadata

ПолеЗначение
IDUC-IDN-003
Контекстidentity (см. docs/06-services/identity/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольИнвестор / Агент / Агентство / МенеджерПИН / СлужбаЗаботы (любой аутентифицированный владелец учётки)
Permissions / scopeidentity.security.manage-self (self-scope; входит в роли investor, agent, agency-owner, pin-manager, care)
Tenantне влияет: операция self-only, record-level по UserId из JWT (ADR-0052)
Record-levelтолько собственная учётка (субъект = актор)

Предусловие

  • Пользователь аутентифицирован (валидный access-JWT), Status = Active.
  • IsTwoFactorEnabled = false (для включения).
  • PhoneNumberConfirmedAt != null (код 2FA уходит на подтверждённый номер учётки).
  • Rate-limit кодов не исчерпан (INV-S4).

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементЛК → «Безопасность» → тумблер «Двухфакторная аутентификация»

Основной Поток

  1. Пользователь включает тумблер 2FA в разделе безопасности ЛК.
  2. Система принимает запрос на включение 2FA (актор — из JWT, тело отсутствует).
  3. Проверить: пользователь существует (для self-операции гарантировано валидным JWT); Status = Active; IsTwoFactorEnabled = false — иначе отказ IDENTITY_TWO_FACTOR_ALREADY_ENABLED; лимит кодов по PhoneNumber учётки не исчерпан (INV-S4, иначе IDENTITY_CONFIRMATION_RATE_LIMITED).
  4. Погасить предыдущий Pending той же цели (Superseded, INV-S1); создать SignInConfirmationRequest (Purpose = EnableTwoFactor, UserId, PhoneNumber = user.PhoneNumber, код CSPRNG хранится хэшем HMAC-SHA256, ExpiresAt = now + 5 мин) (INV-S8: NewEmail / NewPhoneNumber запрещены при этой цели).
  5. Зафиксировать факт: событие ConfirmationCodeRequestedEvent → отправка СМС verifier-ом.
  6. Система возвращает { twoFactorConfirmationRequestId, realConfirmationCodeDispatched }.
  7. Интерфейс показывает поле кода; пользователь вводит код из СМС.
  8. Система принимает подтверждение ConfirmEnableTwoFactorForm { code (1..16) } (привязка — по актору + активному Pending-запросу цели EnableTwoFactor).
  9. Найти активный запрос по (UserId, Purpose = EnableTwoFactor, Status = Pending); отсутствие → IDENTITY_CONFIRMATION_EXPIRED. Проверить ExpiresAt > now (INV-S3); увеличить счётчик попыток ДО сравнения (INV-S2); сравнить код в постоянном времени (INV-S5).
  10. Подтвердить запрос (Pending → Confirmed); включить 2FA у пользователя: IsTwoFactorEnabled = true, TwoFactorChangedAt = now (INV-7: только после подтверждения кодом); Version + 1. Подтверждение запроса и изменение учётки — в одном согласованном изменении (гонка двойного confirm разрешается оптимистичной блокировкой запроса).
  11. Зафиксировать факт: событие UserSecurityChangedEvent (вид = TwoFactorEnabled).
  12. Система возвращает пустой успех; интерфейс обновляет isTwoFactorEnabled, тумблер фиксируется во включённом состоянии.
  13. Со следующего входа по паролю/пин-коду срабатывает ветка 2FA UC-IDN-002.

Альтернативные Потоки

A1. Неверный код

  1. Шаг 9: код не совпал → счётчик попыток увеличен, IDENTITY_CONFIRMATION_INVALID; 5-я попытка → Exhausted (IDENTITY_CONFIRMATION_EXHAUSTED). Возврат к шагу 7 или повторный запрос кода (шаг 2).

A2. Код истёк / пользователь закрыл модалку

  1. ExpiresAt <= nowIDENTITY_CONFIRMATION_EXPIRED; retention-job переводит Pending → Expired.
  2. IsTwoFactorEnabled не изменился. Повторное включение — с шага 1.

A3. Выключение 2FA

  1. Предусловие: IsTwoFactorEnabled = true. Поток зеркален: Purpose = DisableTwoFactor, подтверждение кодом → выключение (IsTwoFactorEnabled = false, TwoFactorChangedAt = now), событие UserSecurityChangedEvent (TwoFactorDisabled).
Почему выключение тоже требует код

Выключение 2FA также требует СМС-код (INV-7): злоумышленник с украденным JWT без доступа к СИМ-карте не может отключить второй фактор и закрепиться в учётке.

A4. Смена телефона при включённом 2FA

  1. Не блокируется: UC-IDN-004 (PhoneChange) сам подтверждается кодом на новый номер; после смены 2FA-коды уходят на новый PhoneNumber.

A5. Параллельные запросы кода и гонка подтверждения

  1. Повторный request включения (шаг 2) до подтверждения гасит предыдущий Pending той же цели (Superseded, INV-S1); действителен только код из последнего запроса — код из погашенного даёт IDENTITY_CONFIRMATION_EXPIRED.
  2. Два параллельных confirm по одному Pending: оптимистичная блокировка запроса пропускает ровно один; проигравший видит уже подтверждённый запрос → идемпотентный успех (2FA уже включена). Повторный вызов request-шага при уже включённой 2FA → IDENTITY_TWO_FACTOR_ALREADY_ENABLED (шаг 3).

Постусловие

  • IsTwoFactorEnabled = true, TwoFactorChangedAt = now; SignInConfirmationRequest в Confirmed.
  • Профиль пользователя возвращает isTwoFactorEnabled: true.
  • Вход по паролю/пин-коду теперь двухшаговый (UC-IDN-002 ветка 2FA).
Активные сессии не отзываются

Активные RefreshSession не отзываются: включение 2FA ужесточает только последующие входы, текущие сессии остаются активными — в отличие от смены телефона (UC-IDN-004), где активные сессии отзываются.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
IDENTITY_TWO_FACTOR_ALREADY_ENABLED409Повторное включениеСинхронизировать состояние тумблера
IDENTITY_CONFIRMATION_RATE_LIMITED429INV-S4Таймер повторной отправки
IDENTITY_CONFIRMATION_INVALID400Неверный код (INV-S5)Ошибка под полем
IDENTITY_CONFIRMATION_EXPIRED400TTL истёк / нет Pending (INV-S3)Кнопка «Отправить код повторно»
IDENTITY_CONFIRMATION_EXHAUSTED4005 попыток (INV-S2)Повторный request
401 UNAUTHORIZED401Нет/просрочен JWTОткрыть форму авторизации

Доменные События

СобытиеКогда поднимаетсяSubscribers
ConfirmationCodeRequestedEventrequest-шагhandler СМС-verifier
UserSecurityChangedEventconfirm-шаг (TwoFactorEnabled/TwoFactorDisabled)in-proc аудит безопасности

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
Интеграционные события не публикуются: флаг 2FA — внутренняя деталь identity, не входит в snapshot UserUpsertedKafkaEvent

Acceptance Criteria

#КритерийПроверка
AC1Включение только после верного кода (INV-7); прямой вызов confirm без request → 400интеграционный тест
AC2После включения вход по паролю возвращает RequiresSmsCode вместо токенасквозной тест с UC-IDN-002
AC35 неверных кодов → Exhausted, флаг не меняетсятест лимита
AC4Выключение тоже требует код (A3)тест disable-потока
AC5isTwoFactorEnabled отражает состояние сразу после confirmтест консистентности
AC62-я отправка кода в окне лимита → IDENTITY_CONFIRMATION_RATE_LIMITED (INV-S4)тест счётчика лимита
AC7Повторный request гасит прежний Pending (Superseded); код из погашенного не подтверждает (INV-S1, A5)два request подряд, confirm первым кодом → fail

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

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

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