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

POST /api/investors/auth/verify-sign-in-password — Проверка пароля при входе (verifySignInPassword)

Назначение

Шаг входа для инвестора с установленным паролем (после шага RequiresPassword): проверка пароля и — в зависимости от 2FA — либо немедленная выдача access-токена, либо создание СМС-запроса подтверждения (nextStep = RequiresSmsCode). Вызывается анонимным клиентом.

Metadata

ПолеЗначение
Сервис/контекстidentity
API areapublic
Feature groupauth
Статусapproved
Документdocs/06-services/identity/api/investor/verify-sign-in-password.md

Актор и доступ

ПроверкаЗначение
Actor typeAnonymous
Auth policy / Permissionsnone
Scope (RBAC)не применяется
Record-levelсвязка по PhoneNumber
Tenant isolationне применяется (ADR-0052)
Auditsecurity-sensitive: попытки проверки пароля логируются (маскированный номер, без пароля)

HTTP-контракт

ПолеЗначение
MethodPOST
Route/api/investors/auth/verify-sign-in-password
Success status200
Idempotency headerне применяется
Correlationсквозной trace через traceparent

Входные данные / параметры запроса (body VerifySignInPasswordForm)

ПолеИсточникТипОбязательностьВалидация / ОграниченияБезопасность
phoneNumberbodystringДаmin 1, max 32; E.164PII, маскирование
passwordbodystringДаmin 1, max 128никогда не логируется
IpAddress / UserAgentmiddlewarestring?Нет≤ 45 / ≤ 512rate-limit / анти-фрод

Модель ответа VerifySignInPasswordCommandResult

Взаимоисключающие ветки ответа

Без 2FA — заполнен accessToken. С 2FA — заполнены nextStep = RequiresSmsCode, signInConfirmationRequestId, realConfirmationCodeDispatched.

ПолеТипОбязательностьИсточникMaskingОписание
nextStepSignInNextStep?Нет (2FA-ветка: RequiresSmsCode)computedСледующий шаг при включённой 2FA.
signInConfirmationRequestIdstring (Guid)?Нет (2FA-ветка)SignInConfirmationRequest.IdДля confirmSignIn.
realConfirmationCodeDispatchedbool?Нет (2FA-ветка)verifierfalse — код реально не отправлялся (тестовый контур).
accessTokenJwtTokenModel?Нет (ветка без 2FA)issue JWT (sub = user.Id)claims без PIIИтоговый токен входа.

Алгоритм

  1. Контекст и доступ. Актор анонимный; из middleware известны IpAddress/UserAgent.

  2. Проверка входа. phoneNumber непустой ≤ 32, password непустой ≤ 128 — иначе ValidationError; нормализация E.164.

  3. Проверки сущностей. Найти пользователя по нормализованному номеру — не найден ⇒ 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-enumeration

    IDENTITY_SIGN_IN_FAILED возвращается одинаково для трёх разных причин — номер не найден, пароль не установлен, пароль неверен, — чтобы анонимный клиент не мог по коду ошибки определить, существует ли учётная запись.

  4. Правила и переходы. Сверка пароля (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.
  5. Что меняется. В одном согласованном изменении обновляется пользователь (счётчики/LastSignInAt); 2FA-ветка дополнительно гасит прежний активный (Pending) запрос той же пары в Superseded (INV-S1) и создаёт новый запрос; неуспешная попытка также сохраняется (счётчик) до возврата ошибки.

  6. События. Ветка без 2FA: UserSignedInKafkaEventpin.identity.sign-ins.v1 (ключ = UserId; UserId, SessionId, SignInMethod = Password, Device.Platform, OccurredAt). 2FA-ветка: интеграционных событий нет — вход завершит confirmSignIn.

  7. Результат. Заполняется ровно одна ветка ответа (см. таблицу); 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); опубликован UserSignedInKafkaEventSessionId); выдан 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, Then 200 {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 запрос, Then IDENTITY_USER_BLOCKED (400).
  • Given LockoutUntil > now, When запрос, Then IDENTITY_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
ValidationError400Пустые/длинные поляСписок ошибок валидацииno retry
IDENTITY_SIGN_IN_FAILED400Номер не найден / пароль не установлен / пароль неверен«Неверные данные входа»no retry
IDENTITY_USER_BLOCKED400Status ∈ {Blocked, Archived}«Учётная запись заблокирована»no retry
IDENTITY_USER_LOCKED_OUT400LockoutUntil > now (INV-8)«Вход временно заблокирован»retry после LockoutUntil
IDENTITY_CONFIRMATION_RATE_LIMITED429INV-S4 (2FA-ветка)«Слишком много запросов кода»retry ≥ 60 сек
IDENTITY_SMS_DISPATCH_FAILED400Отказ verifier (2FA-ветка)«Не удалось отправить код»retry

Совместимость и наблюдаемость

  • Обе ветки ответа nullable — клиент ветвится по наличию accessToken. Новые optional поля — additive.
  • Logs: результат сверки (bool), 2FA-ветка (bool), маскированный номер; metrics: identity_password_verify_total{result,two_factor}; traces — OTel.

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

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

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