UC-IDN-002. Вход по паролю вместо СМС
Назначение
Вход существующего пользователя по паролю (или пин-коду) вместо СМС-кода. Вызывает гость, у чьей учётки
установлен пароль. При включённой двухфакторной аутентификации (IsTwoFactorEnabled = true) верный пароль
не завершает вход, а требует дополнительного СМС-кода. На выходе — access-токен либо переход на СМС-шаг.
Связан с контекстом identity.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-IDN-002 |
| Контекст | identity (см. docs/06-services/identity/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | Гость (аутентифицирующийся владелец учётки: Инвестор / Агент / Девелопер / МенеджерПИН / СлужбаЗаботы) |
| Permissions / scope | permissions не требуются (анонимная операция входа); установка пароля ранее — identity.security.manage-self |
| Tenant | null (инвестор) или 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 UC-IDN-001 ввёл телефон; система вернула
nextStep = RequiresPassword(у учёткиhasPassword = true,sendSmsCodeне запрошен). - Интерфейс показывает поле пароля; пользователь вводит пароль.
- Система принимает запрос
VerifySignInPasswordForm { phoneNumber (1..32), password (1..128) }от гостя. - Проверить вход: формат полей; найти пользователя по номеру (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). - Проверить пароль (Argon2id, постоянное время). При неуспехе — увеличить
FailedAccessAttempts; при достижении 10 установитьLockoutUntil = now + 15 мин(user.md) и вернуть отказIDENTITY_INVALID_CURRENT_PASSWORD(единый код, чтобы не раскрывать существование учётки). При успехе — сбросить счётчик. - Разветвить по
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(поля результата взаимоисключающие по ветке).
- Зафиксировать факты: при завершённом входе —
UserSignedInEventи интеграционноеUserSignedInKafkaEvent(SignInMethod = Password); при ветке 2FA —ConfirmationCodeRequestedEvent. Обновление учётки и публикация события — в одном согласованном изменении. - (Только при 2FA) Пользователь вводит СМС-код; система подтверждает его — шаги 10–13 UC-IDN-001;
SignInMethodитоговой сессии =Password(вход инициирован паролем). - Интерфейс сохраняет
accessToken, обновляет данные текущего пользователя. user-activityполучаетUserSignedInKafkaEvent, обновляет витрину сессий.
Альтернативные Потоки
A1. Вход по пин-коду
- Система вернула
nextStep = RequiresPinCode(hasPinCode = true). - Интерфейс отправляет подтверждение с
pinCode(≤16, формат^\d{4,12}$) безsignInConfirmationRequestId. - Система проверяет
PinCodeHash(Argon2id) с теми же счётчиком/lockout; приIsTwoFactorEnabled = true— ветка СМС как в основном потоке;SignInMethod = PinCode. Завершается шагом 9.
A2. Неверный пароль (< 10 попыток)
- Шаг 5:
FailedAccessAttempts + 1, отказ. Интерфейс показывает ошибку, остаток до блокировки не раскрывает. Возврат к шагу 2.
A3. 10-я неверная попытка — lockout
LockoutUntil = now + 15 мин; последующие попытки (даже с верным паролем) →IDENTITY_USER_LOCKED_OUT(423) до истечения. Пользователь может дождаться или войти по СМС после истечения lockout (lockout действует на все способы confirm — INV-8).
A4. Пользователь забыл пароль
- Пользователь выбирает «Войти по СМС» — повторная отправка кода → далее UC-IDN-001 шаги 9–16.
- После входа пользователь может сменить пароль (
currentPasswordобязателен приhasPassword = true— INV-6; забыт → сброс пароля через поддержку, self-reset вне scope).
A5. Конкурентные неверные попытки
- Параллельные проверки пароля инкрементируют
FailedAccessAttempts; при пересечении порога 10LockoutUntil = now + 15 минустанавливается один раз (оптимистическая блокировка по версии учётки — счётчик не «перескакивает» порог из-за гонки). - Проигравшие параллельные запросы получают
IDENTITY_INVALID_CURRENT_PASSWORD, а после установки lockout —IDENTITY_USER_LOCKED_OUT(423).
A6. Брошенная 2FA-ветка
- Пароль верен,
IsTwoFactorEnabled = true: созданSignInConfirmationRequest (Purpose = SignIn), отправлена СМС, но пользователь не вводит код.RefreshSessionне создаётся (сессия рождается только на confirm), доступ не выдан,LastSignInAtне меняется. - Повторный вход паролем создаёт новый
Pending(старый →Superseded, INV-S1). Вход считается успешным только после подтверждения кода.
Постусловие
- Выдан access-JWT; создана
RefreshSession (Active, SignInMethod = Password | PinCode). FailedAccessAttempts = 0,LastSignInAt = now.- При ветке 2FA: промежуточный
SignInConfirmationRequestвConfirmed.
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
ValidationError | 400 | Пустые/сверхдлинные поля | Подсветить поле |
IDENTITY_INVALID_CURRENT_PASSWORD | 400 | Пароль/пин не совпал (единый код, чтобы не раскрывать существование учётки) | «Неверный телефон или пароль» |
IDENTITY_USER_LOCKED_OUT | 423 | LockoutUntil > now (INV-8) | Время разблокировки, предложение СМС-входа позже |
IDENTITY_USER_BLOCKED | 403 | Status = Blocked | «Обратитесь в поддержку» |
IDENTITY_COMPANY_SUSPENDED | 403 | Компания Suspended (INV-9) | «Доступ компании приостановлен» |
IDENTITY_CONFIRMATION_* | 400/429 | Ветка 2FA — ошибки кода (INV-S2..S5) | Как в UC-IDN-001 |
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
UserSignedInEvent | Успешная верификация (без 2FA) или confirm 2FA-кода | in-proc аудит |
ConfirmationCodeRequestedEvent | Ветка 2FA | handler СМС-verifier |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
UserSignedInKafkaEvent.v1 | pin.identity.sign-ins.v1 | Завершённый вход (SignInMethod = Password/PinCode) | user-activity, notifications (алерт нового устройства) |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Верный пароль без 2FA → accessToken одним запросом | интеграционный тест |
| AC2 | Верный пароль с 2FA → nextStep = RequiresSmsCode, токен только после confirm | тест ветки 2FA |
| AC3 | 10 неверных паролей → lockout 15 мин, 423 даже на верный пароль | тест счётчика |
| AC4 | Успешный вход сбрасывает FailedAccessAttempts | тест: 9 неудач → успех → счётчик 0 |
| AC5 | Ответ о неверном пароле не отличается для несуществующего номера | тест анти-enumeration |
| AC6 | Пин-код входит тем же контрактом подтверждения (pinCode) | тест A1 |
| AC7 | Конкурентные неверные попытки не проскакивают порог lockout; после 10 — 423 | тест гонки счётчика FailedAccessAttempts |
| AC8 | 2FA-ветка без ввода кода не создаёт сессию; повтор входа гасит прежний Pending (Superseded) | тест брошенной 2FA-ветки |
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (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 метод:
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).