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

UC-IDN-002. Вход по паролю вместо СМС

Назначение

Вход существующего пользователя по паролю (или пин-коду) вместо СМС-кода. Вызывает гость, у чьей учётки установлен пароль. При включённой двухфакторной аутентификации (IsTwoFactorEnabled = true) верный пароль не завершает вход, а требует дополнительного СМС-кода. На выходе — access-токен либо переход на СМС-шаг. Связан с контекстом identity.

Metadata

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

Актор

ПолеЗначение
РольГость (аутентифицирующийся владелец учётки: Инвестор / Агент / Девелопер / МенеджерПИН / СлужбаЗаботы)
Permissions / scopepermissions не требуются (анонимная операция входа); установка пароля ранее — identity.security.manage-self
Tenantnull (инвестор) или CompanyId пользователя (ADR-0052)
Record-levelучётка по PhoneNumber; счётчик FailedAccessAttempts — на учётке

Предусловие

  • Учётка существует, PasswordHash != null (пароль установлен ранее; hasPassword = true).
  • UserEntity.Status = Active (или Invited — недостижимо: пароль ставится после первого входа).
  • LockoutUntil не в будущем (INV-8); для пользователя компании Company.Status = Active (INV-9).

Триггер

ПолеЗначение
ИсточникUI button
Триггерящий элементФорма авторизации: шаг «Введите пароль» после входа/регистрации → nextStep = RequiresPassword

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

  1. Пользователь на шаге 1 UC-IDN-001 ввёл телефон; система вернула nextStep = RequiresPassword (у учётки hasPassword = true, sendSmsCode не запрошен).
  2. Интерфейс показывает поле пароля; пользователь вводит пароль.
  3. Система принимает запрос VerifySignInPasswordForm { phoneNumber (1..32), password (1..128) } от гостя.
  4. Проверить вход: формат полей; найти пользователя по номеру (E.164). Если не найден — отказ IDENTITY_CONFIRMATION_INVALID (без раскрытия существования учётки). Проверить guard-ы: Status ∉ {Blocked, Archived} (иначе IDENTITY_USER_BLOCKED), LockoutUntil не в будущем (INV-8, иначе IDENTITY_USER_LOCKED_OUT), PasswordHash != null; для пользователя компании — компания Active (INV-9, иначе IDENTITY_COMPANY_SUSPENDED).
  5. Проверить пароль (Argon2id, постоянное время). При неуспехе — увеличить FailedAccessAttempts; при достижении 10 установить LockoutUntil = now + 15 мин (user.md) и вернуть отказ IDENTITY_INVALID_CURRENT_PASSWORD (единый код, чтобы не раскрывать существование учётки). При успехе — сбросить счётчик.
  6. Разветвить по IsTwoFactorEnabled:
    • false → выдать access-JWT, создать RefreshSession (SignInMethod = Password), LastSignInAt = now; результат { accessToken }, nextStep = null;
    • true → создать SignInConfirmationRequest (Purpose = SignIn) (INV-S1/S4 как в UC-IDN-001), отправить СМС; результат { nextStep = RequiresSmsCode, signInConfirmationRequestId, realConfirmationCodeDispatched }, accessToken = null (поля результата взаимоисключающие по ветке).
  7. Зафиксировать факты: при завершённом входе — UserSignedInEvent и интеграционное UserSignedInKafkaEvent (SignInMethod = Password); при ветке 2FA — ConfirmationCodeRequestedEvent. Обновление учётки и публикация события — в одном согласованном изменении.
  8. (Только при 2FA) Пользователь вводит СМС-код; система подтверждает его — шаги 10–13 UC-IDN-001; SignInMethod итоговой сессии = Password (вход инициирован паролем).
  9. Интерфейс сохраняет accessToken, обновляет данные текущего пользователя.
  10. user-activity получает UserSignedInKafkaEvent, обновляет витрину сессий.

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

A1. Вход по пин-коду

  1. Система вернула nextStep = RequiresPinCode (hasPinCode = true).
  2. Интерфейс отправляет подтверждение с pinCode (≤16, формат ^\d{4,12}$) без signInConfirmationRequestId.
  3. Система проверяет PinCodeHash (Argon2id) с теми же счётчиком/lockout; при IsTwoFactorEnabled = true — ветка СМС как в основном потоке; SignInMethod = PinCode. Завершается шагом 9.

A2. Неверный пароль (< 10 попыток)

  1. Шаг 5: FailedAccessAttempts + 1, отказ. Интерфейс показывает ошибку, остаток до блокировки не раскрывает. Возврат к шагу 2.

A3. 10-я неверная попытка — lockout

  1. LockoutUntil = now + 15 мин; последующие попытки (даже с верным паролем) → IDENTITY_USER_LOCKED_OUT (423) до истечения. Пользователь может дождаться или войти по СМС после истечения lockout (lockout действует на все способы confirm — INV-8).

A4. Пользователь забыл пароль

  1. Пользователь выбирает «Войти по СМС» — повторная отправка кода → далее UC-IDN-001 шаги 9–16.
  2. После входа пользователь может сменить пароль (currentPassword обязателен при hasPassword = true — INV-6; забыт → сброс пароля через поддержку, self-reset вне scope).

A5. Конкурентные неверные попытки

  1. Параллельные проверки пароля инкрементируют FailedAccessAttempts; при пересечении порога 10 LockoutUntil = now + 15 мин устанавливается один раз (оптимистическая блокировка по версии учётки — счётчик не «перескакивает» порог из-за гонки).
  2. Проигравшие параллельные запросы получают IDENTITY_INVALID_CURRENT_PASSWORD, а после установки lockout — IDENTITY_USER_LOCKED_OUT (423).

A6. Брошенная 2FA-ветка

  1. Пароль верен, IsTwoFactorEnabled = true: создан SignInConfirmationRequest (Purpose = SignIn), отправлена СМС, но пользователь не вводит код. RefreshSession не создаётся (сессия рождается только на confirm), доступ не выдан, LastSignInAt не меняется.
  2. Повторный вход паролем создаёт новый Pending (старый → Superseded, INV-S1). Вход считается успешным только после подтверждения кода.

Постусловие

  • Выдан access-JWT; создана RefreshSession (Active, SignInMethod = Password | PinCode).
  • FailedAccessAttempts = 0, LastSignInAt = now.
  • При ветке 2FA: промежуточный SignInConfirmationRequest в Confirmed.

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

КодHTTPУсловиеПоведение клиента
ValidationError400Пустые/сверхдлинные поляПодсветить поле
IDENTITY_INVALID_CURRENT_PASSWORD400Пароль/пин не совпал (единый код, чтобы не раскрывать существование учётки)«Неверный телефон или пароль»
IDENTITY_USER_LOCKED_OUT423LockoutUntil > now (INV-8)Время разблокировки, предложение СМС-входа позже
IDENTITY_USER_BLOCKED403Status = Blocked«Обратитесь в поддержку»
IDENTITY_COMPANY_SUSPENDED403Компания Suspended (INV-9)«Доступ компании приостановлен»
IDENTITY_CONFIRMATION_*400/429Ветка 2FA — ошибки кода (INV-S2..S5)Как в UC-IDN-001

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

СобытиеКогда поднимаетсяSubscribers
UserSignedInEventУспешная верификация (без 2FA) или confirm 2FA-кодаin-proc аудит
ConfirmationCodeRequestedEventВетка 2FAhandler СМС-verifier

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

СобытиеTopicКогда публикуетсяConsumers
UserSignedInKafkaEvent.v1pin.identity.sign-ins.v1Завершённый вход (SignInMethod = Password/PinCode)user-activity, notifications (алерт нового устройства)

Acceptance Criteria

#КритерийПроверка
AC1Верный пароль без 2FA → accessToken одним запросоминтеграционный тест
AC2Верный пароль с 2FA → nextStep = RequiresSmsCode, токен только после confirmтест ветки 2FA
AC310 неверных паролей → lockout 15 мин, 423 даже на верный парольтест счётчика
AC4Успешный вход сбрасывает FailedAccessAttemptsтест: 9 неудач → успех → счётчик 0
AC5Ответ о неверном пароле не отличается для несуществующего номератест анти-enumeration
AC6Пин-код входит тем же контрактом подтверждения (pinCode)тест A1
AC7Конкурентные неверные попытки не проскакивают порог lockout; после 10 — 423тест гонки счётчика FailedAccessAttempts
AC82FA-ветка без ввода кода не создаёт сессию; повтор входа гасит прежний Pending (Superseded)тест брошенной 2FA-ветки

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

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

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

  • API метод: identity/api/investor/verify-sign-in-password.md, identity/api/investor/confirm-sign-in.md, identity/api/investor/change-password.md.
  • Domain entity: ../domain/user.md (INV-6/8), ../domain/user-session.md.
  • Связанные UC: UC-IDN-001 (СМС-flow), UC-IDN-003 (включение 2FA).