POST /api/investors/auth/verify-sign-in-password — Проверка пароля при входе (verifySignInPassword)
Назначение
Шаг входа для инвестора с установленным паролем (после шага RequiresPassword): проверка пароля и — в
зависимости от 2FA — либо немедленная выдача access-токена, либо создание СМС-запроса подтверждения
(nextStep = RequiresSmsCode). Вызывается анонимным клиентом.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | auth |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/verify-sign-in-password.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | Anonymous |
| Auth policy / Permissions | none |
| Scope (RBAC) | не применяется |
| Record-level | связка по PhoneNumber |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | security-sensitive: попытки проверки пароля логируются (маскированный номер, без пароля) |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investors/auth/verify-sign-in-password |
| Success status | 200 |
| Idempotency header | не применяется |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса (body VerifySignInPasswordForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
phoneNumber | body | string | Да | min 1, max 32; E.164 | PII, маскирование |
password | body | string | Да | min 1, max 128 | никогда не логируется |
IpAddress / UserAgent | middleware | string? | Нет | ≤ 45 / ≤ 512 | rate-limit / анти-фрод |
Модель ответа VerifySignInPasswordCommandResult
Без 2FA — заполнен accessToken. С 2FA — заполнены nextStep = RequiresSmsCode,
signInConfirmationRequestId, realConfirmationCodeDispatched.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
nextStep | SignInNextStep? | Нет (2FA-ветка: RequiresSmsCode) | computed | — | Следующий шаг при включённой 2FA. |
signInConfirmationRequestId | string (Guid)? | Нет (2FA-ветка) | SignInConfirmationRequest.Id | — | Для confirmSignIn. |
realConfirmationCodeDispatched | bool? | Нет (2FA-ветка) | verifier | — | false — код реально не отправлялся (тестовый контур). |
accessToken | JwtTokenModel? | Нет (ветка без 2FA) | issue JWT (sub = user.Id) | claims без PII | Итоговый токен входа. |
Алгоритм
-
Контекст и доступ. Актор анонимный; из middleware известны
IpAddress/UserAgent. -
Проверка входа.
phoneNumberнепустой ≤ 32,passwordнепустой ≤ 128 — иначеValidationError; нормализация E.164. -
Проверки сущностей. Найти пользователя по нормализованному номеру — не найден ⇒
IDENTITY_SIGN_IN_FAILED(анти user-enumeration; тот же код, что и при неверном пароле). Состояние:Status ∈ {Active, Invited}— иначеIDENTITY_USER_BLOCKED; блокировка входа снята (LockoutUntil <= now, INV-8) — иначеIDENTITY_USER_LOCKED_OUT; пароль установлен — иначеIDENTITY_SIGN_IN_FAILED.Анти user-enumerationIDENTITY_SIGN_IN_FAILEDвозвращается одинаково для трёх разных причин — номер не найден, пароль не установлен, пароль неверен, — чтобы анонимный клиент не мог по коду ошибки определить, существует ли учётная запись. -
Правила и переходы. Сверка пароля (Argon2id):
- неуспех — счётчик неудачных попыток увеличивается; при достижении 10 — блокировка входа на 15 мин
(INV-8); отказ
IDENTITY_SIGN_IN_FAILED; - успех, 2FA выключена — счётчик обнуляется,
LastSignInAt = now,Invited → Active; фиксируется доменное событиеUserSignedInEvent(способPassword); выпускается access-JWT →accessToken; - успех, 2FA включена (INV-7-семейство) — токен не выдаётся; сгенерировать код (CSPRNG); создать
запрос подтверждения (цель
SignIn,UserId,PhoneNumber = телефон пользователя, хэш кода,ExpiresAt = now + 5 мин,IpAddress,UserAgent); rate-limit INV-S4; доменноеConfirmationCodeRequestedEvent→ СМС; ответ:nextStep = RequiresSmsCode+requestId.
- неуспех — счётчик неудачных попыток увеличивается; при достижении 10 — блокировка входа на 15 мин
(INV-8); отказ
-
Что меняется. В одном согласованном изменении обновляется пользователь (счётчики/
LastSignInAt); 2FA-ветка дополнительно гасит прежний активный (Pending) запрос той же пары вSuperseded(INV-S1) и создаёт новый запрос; неуспешная попытка также сохраняется (счётчик) до возврата ошибки. -
События. Ветка без 2FA:
UserSignedInKafkaEvent→pin.identity.sign-ins.v1(ключ =UserId;UserId,SessionId,SignInMethod = Password,Device.Platform,OccurredAt). 2FA-ветка: интеграционных событий нет — вход завершитconfirmSignIn. -
Результат. Заполняется ровно одна ветка ответа (см. таблицу); HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | UserSignedInKafkaEvent / pin.identity.sign-ins.v1 — только в ветке без 2FA |
| Внешние вызовы | СМС-verifier — только в 2FA-ветке |
| Проекции / поиск | rate-limit-счётчик (2FA-ветка) |
| Уведомления | СМС с кодом (2FA-ветка) |
Кросс-сервисный контракт и окно согласованности
- Предоставляет (только ветка без 2FA):
UserSignedInKafkaEventв топикpin.identity.sign-ins.v1,ContractVersion = 1, ключ =UserId; снапшот{UserId, SessionId, SignInMethod = Password, Device.Platform, OccurredAt}. Потребители:user-activity(витрина сессий/устройств, связывание anonymous-сессии),notifications(алерт о входе с нового устройства). - Ожидает от других: ничего синхронно, кроме внешнего СМС-verifier (только в 2FA-ветке).
- Окно согласованности: access-токен действует немедленно; появление входа в витрине устройств и алерт notifications — асинхронно (лаг — секунды). 2FA-ветка интеграционных событий не порождает — вход завершит confirm-sign-in.md.
Предусловия и постусловия
- Пред: актор анонимный;
phoneNumber(E.164) иpasswordзаданы; учёткаActive/Invited, без активного lockout (INV-8), пароль установлен. - Пост (успех, 2FA выкл.):
FailedAccessAttempts = 0,LastSignInAt = now,Invited → Active; зафиксированUserSignedInEvent(способPassword); опубликованUserSignedInKafkaEvent(сSessionId); выдан access-JWT. - Пост (успех, 2FA вкл.): токен не выдаётся; создан
Pending-запросSignIn(прежний →Superseded, INV-S1), код отправлен,nextStep = RequiresSmsCode. - Пост (неуспех пароля):
FailedAccessAttemptsувеличен; при 10-м подряд —LockoutUntil = now + 15 мин(INV-8); отказIDENTITY_SIGN_IN_FAILED.
Acceptance Criteria
- Given инвестор с паролем и выключенной 2FA, верный пароль, When
POST /verify-sign-in-password, Then200{accessToken}; счётчик неудач обнулён,LastSignInAt = now,Invited → Active; опубликованUserSignedInKafkaEvent(key =UserId,SignInMethod = Password). - Given инвестор с паролем и включённой 2FA, верный пароль, When запрос, Then
200{nextStep = RequiresSmsCode, signInConfirmationRequestId, realConfirmationCodeDispatched}без токена; созданPending-запросSignIn, отправлен код; интеграционных событий нет. - Given неверный пароль, When запрос, Then
FailedAccessAttemptsувеличивается, отказIDENTITY_SIGN_IN_FAILED(400). - Given 10-я подряд неверная попытка, When запрос, Then ставится
LockoutUntil = now + 15 мин(INV-8); последующие попытки до истечения →IDENTITY_USER_LOCKED_OUT(400). - Given номер не зарегистрирован или пароль не установлен, When запрос, Then тот же
IDENTITY_SIGN_IN_FAILED(400, анти user-enumeration — неотличимо от неверного пароля). - Given учётка
Blocked/Archived, When запрос, ThenIDENTITY_USER_BLOCKED(400). - Given
LockoutUntil > now, When запрос, ThenIDENTITY_USER_LOCKED_OUT(400). - Given 2FA-ветка и превышен лимит СМС (INV-S4), When запрос, Then
IDENTITY_CONFIRMATION_RATE_LIMITED(429). - Given 2FA-ветка и отказ verifier, When запрос, Then
IDENTITY_SMS_DISPATCH_FAILED(400).
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
ValidationError | 400 | Пустые/длинные поля | Список ошибок валидации | no retry |
IDENTITY_SIGN_IN_FAILED | 400 | Номер не найден / пароль не установлен / пароль неверен | «Неверные данные входа» | no retry |
IDENTITY_USER_BLOCKED | 400 | Status ∈ {Blocked, Archived} | «Учётная запись заблокирована» | no retry |
IDENTITY_USER_LOCKED_OUT | 400 | LockoutUntil > now (INV-8) | «Вход временно заблокирован» | retry после LockoutUntil |
IDENTITY_CONFIRMATION_RATE_LIMITED | 429 | INV-S4 (2FA-ветка) | «Слишком много запросов кода» | retry ≥ 60 сек |
IDENTITY_SMS_DISPATCH_FAILED | 400 | Отказ verifier (2FA-ветка) | «Не удалось отправить код» | retry |
Совместимость и наблюдаемость
- Обе ветки ответа nullable — клиент ветвится по наличию
accessToken. Новые optional поля — additive. - Logs: результат сверки (bool), 2FA-ветка (bool), маскированный номер; metrics:
identity_password_verify_total{result,two_factor}; traces — OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (6): Identity API — карта областей и реестр investor-области, POST /api/investors/auth/confirm-sign-in — Подтверждение входа (confirmSignIn), POST /api/investors/auth/request-sign-in-code — Первый шаг входа (requestSignInCode), POST /api/investors/auth/sign-up-or-sign-in — Войти или зарегистрироваться (signUpOrSignIn), POST /api/investors/me/2fa/enable-confirm — Подтверждение включения 2FA (confirmEnableTwoFactor), POST /api/investors/me/password/change — Смена/установка пароля (changePassword)
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущности: user.md (INV-6/7/8), user-session.md (INV-S1/S4).
- Flow: пред-шаг request-sign-in-code.md / sign-up-or-sign-in.md; 2FA-продолжение — confirm-sign-in.md.