UC-IDN-003. Включение двухфакторной аутентификации
Назначение
Пользователь включает двухфакторную аутентификацию (2FA) в личном кабинете: запрашивает код, подтверждает
его, после чего вход по паролю/пин-коду дополнительно требует СМС-код. Вызывает любой аутентифицированный
владелец учётки. Выключение — зеркальный поток, зафиксирован альтернативой A3. Связан с контекстом
identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-003 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Инвестор / Агент / Агентство / МенеджерПИН / СлужбаЗаботы (любой аутентифицированный владелец учётки) |
| Permissions / scope | identity.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 |
| Триггерящий элемент | ЛК → «Безопасность» → тумблер «Двухфакторная аутентификация» |
Основной Поток
- Пользователь включает тумблер 2FA в разделе безопасности ЛК.
- Система принимает запрос на включение 2FA (актор — из JWT, тело отсутствует).
- Проверить: пользователь существует (для self-операции гарантировано валидным JWT);
Status = Active;IsTwoFactorEnabled = false— иначе отказIDENTITY_TWO_FACTOR_ALREADY_ENABLED; лимит кодов поPhoneNumberучётки не исчерпан (INV-S4, иначеIDENTITY_CONFIRMATION_RATE_LIMITED). - Погасить предыдущий Pending той же цели (
Superseded, INV-S1); создатьSignInConfirmationRequest (Purpose = EnableTwoFactor, UserId, PhoneNumber = user.PhoneNumber, код CSPRNG хранится хэшем HMAC-SHA256, ExpiresAt = now + 5 мин)(INV-S8:NewEmail/NewPhoneNumberзапрещены при этой цели). - Зафиксировать факт: событие
ConfirmationCodeRequestedEvent→ отправка СМС verifier-ом. - Система возвращает
{ twoFactorConfirmationRequestId, realConfirmationCodeDispatched }. - Интерфейс показывает поле кода; пользователь вводит код из СМС.
- Система принимает подтверждение
ConfirmEnableTwoFactorForm { code (1..16) }(привязка — по актору + активному Pending-запросу целиEnableTwoFactor). - Найти активный запрос по (
UserId,Purpose = EnableTwoFactor,Status = Pending); отсутствие →IDENTITY_CONFIRMATION_EXPIRED. ПроверитьExpiresAt > now(INV-S3); увеличить счётчик попыток ДО сравнения (INV-S2); сравнить код в постоянном времени (INV-S5). - Подтвердить запрос (
Pending → Confirmed); включить 2FA у пользователя:IsTwoFactorEnabled = true,TwoFactorChangedAt = now(INV-7: только после подтверждения кодом);Version + 1. Подтверждение запроса и изменение учётки — в одном согласованном изменении (гонка двойного confirm разрешается оптимистичной блокировкой запроса). - Зафиксировать факт: событие
UserSecurityChangedEvent (вид = TwoFactorEnabled). - Система возвращает пустой успех; интерфейс обновляет
isTwoFactorEnabled, тумблер фиксируется во включённом состоянии. - Со следующего входа по паролю/пин-коду срабатывает ветка 2FA UC-IDN-002.
Альтернативные Потоки
A1. Неверный код
- Шаг 9: код не совпал → счётчик попыток увеличен,
IDENTITY_CONFIRMATION_INVALID; 5-я попытка →Exhausted(IDENTITY_CONFIRMATION_EXHAUSTED). Возврат к шагу 7 или повторный запрос кода (шаг 2).
A2. Код истёк / пользователь закрыл модалку
ExpiresAt <= now→IDENTITY_CONFIRMATION_EXPIRED; retention-job переводит Pending →Expired.IsTwoFactorEnabledне изменился. Повторное включение — с шага 1.
A3. Выключение 2FA
- Предусловие:
IsTwoFactorEnabled = true. Поток зеркален:Purpose = DisableTwoFactor, подтверждение кодом → выключение (IsTwoFactorEnabled = false,TwoFactorChangedAt = now), событиеUserSecurityChangedEvent (TwoFactorDisabled).
Почему выключение тоже требует код
Выключение 2FA также требует СМС-код (INV-7): злоумышленник с украденным JWT без доступа к СИМ-карте не может отключить второй фактор и закрепиться в учётке.
A4. Смена телефона при включённом 2FA
- Не блокируется: UC-IDN-004 (
PhoneChange) сам подтверждается кодом на новый номер; после смены 2FA-коды уходят на новыйPhoneNumber.
A5. Параллельные запросы кода и гонка подтверждения
- Повторный request включения (шаг 2) до подтверждения гасит предыдущий Pending той же цели
(
Superseded, INV-S1); действителен только код из последнего запроса — код из погашенного даётIDENTITY_CONFIRMATION_EXPIRED. - Два параллельных 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_ENABLED | 409 | Повторное включение | Синхронизировать состояние тумблера |
IDENTITY_CONFIRMATION_RATE_LIMITED | 429 | INV-S4 | Таймер повторной отправки |
IDENTITY_CONFIRMATION_INVALID | 400 | Неверный код (INV-S5) | Ошибка под полем |
IDENTITY_CONFIRMATION_EXPIRED | 400 | TTL истёк / нет Pending (INV-S3) | Кнопка «Отправить код повторно» |
IDENTITY_CONFIRMATION_EXHAUSTED | 400 | 5 попыток (INV-S2) | Повторный request |
401 UNAUTHORIZED | 401 | Нет/просрочен JWT | Открыть форму авторизации |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
ConfirmationCodeRequestedEvent | request-шаг | handler СМС-verifier |
UserSecurityChangedEvent | confirm-шаг (TwoFactorEnabled/TwoFactorDisabled) | in-proc аудит безопасности |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
| — | — | Интеграционные события не публикуются: флаг 2FA — внутренняя деталь identity, не входит в snapshot UserUpsertedKafkaEvent | — |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Включение только после верного кода (INV-7); прямой вызов confirm без request → 400 | интеграционный тест |
| AC2 | После включения вход по паролю возвращает RequiresSmsCode вместо токена | сквозной тест с UC-IDN-002 |
| AC3 | 5 неверных кодов → Exhausted, флаг не меняется | тест лимита |
| AC4 | Выключение тоже требует код (A3) | тест disable-потока |
| AC5 | isTwoFactorEnabled отражает состояние сразу после confirm | тест консистентности |
| AC6 | 2-я отправка кода в окне лимита → IDENTITY_CONFIRMATION_RATE_LIMITED (INV-S4) | тест счётчика лимита |
| AC7 | Повторный request гасит прежний Pending (Superseded); код из погашенного не подтверждает (INV-S1, A5) | два request подряд, confirm первым кодом → fail |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (1): Entity / Aggregate: ProfileEntity
- Use Cases (2): UC-PRF-005. Запрет редактирования профиля для менеджера застройщика (TalentSync), UC-PRF-006. Смена email/phone из ЛК (делегирование в identity + обновление реплики)
- index.md (2): Identity Service (Pin.Identity), Profile Service (Pin.Profile)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- API методы: request-enable-two-factor.md, confirm-enable-two-factor.md, request-disable-two-factor.md, confirm-disable-two-factor.md.
- Domain entity: ../domain/user.md (INV-7),
../domain/user-session.md (
ConfirmationPurpose = EnableTwoFactor/DisableTwoFactor). - Permission:
identity.security.manage-self— ../domain/permission.md. - Связанные UC: UC-IDN-002 (эффект 2FA во входе), UC-IDN-004 (та же механика confirm).