POST /api/investors/me/password/change — Смена/установка пароля (changePassword)
Назначение
Сменить или впервые установить пароль текущего пользователя, чтобы разрешить вход по паролю вместо
кода из СМС. Вызывается аутентифицированным пользователем из личного кабинета. Если пароль уже установлен
(hasPassword = true) — обязателен текущий пароль (INV-6). После установки вход предлагает шаг
RequiresPassword.
Metadata
| Поле | Значение |
|---|---|
| Сервис/контекст | identity |
| API area | public |
| Feature group | security |
| Статус | approved |
| Документ | docs/06-services/identity/api/investor/change-password.md |
Актор и доступ
| Проверка | Значение |
|---|---|
| Actor type | User (Bearer JWT) |
| Auth policy / Permissions | authenticated; permission identity.security.manage-self |
| Scope (RBAC) | self |
| Record-level | только собственная запись (UserId из sub) |
| Tenant isolation | не применяется (ADR-0052) |
| Audit | security-change: событие UserSecurityChangedEvent, actor-stamping |
HTTP-контракт
| Поле | Значение |
|---|---|
| Method | POST |
| Route | /api/investors/me/password/change |
| Success status | 200 |
| Idempotency header | не применяется |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса (body ChangePasswordForm)
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|---|---|---|---|---|
UserId | identity claims (sub) | Guid | Да | валидный JWT | 401 без токена |
currentPassword | body | string? | Условно: обязателен при установленном пароле (INV-6) | max 128 | никогда не логируется |
newPassword | body | string | Да | min 8, max 128 (INV-6 PasswordPolicy) | никогда не логируется |
Модель ответа ChangePasswordCommandResult
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|---|---|---|---|---|
investorId | string (Guid) | Да | UserEntity.Id | — | Идентификатор пользователя. |
Алгоритм
- Контекст и доступ. Операцию выполняет аутентифицированный пользователь над собственной записью
(
UserIdиз токена). - Проверка входа.
newPasswordнепустой, длина 8–128;currentPassword≤ 128 — иначеValidationError. - Проверки состояния. Получить пользователя; он
Active— иначеIDENTITY_USER_BLOCKED; блокировка входа снята (LockoutUntil <= now, INV-8) — иначеIDENTITY_USER_LOCKED_OUT(защита от перебора текущего пароля). - Правила и переходы.
- если пароль уже установлен:
currentPasswordобязателен и должен пройти сверку; неуспех — счётчик неудачных попыток увеличивается (10 подряд ⇒ блокировка входа на 15 мин) и отказIDENTITY_INVALID_CURRENT_PASSWORD; - если пароль ещё не установлен (первая установка):
currentPasswordне требуется; - при успехе: сохраняется новый хэш пароля (Argon2id),
PasswordChangedAt = now, счётчик неудачных попыток обнуляется; - фиксируется доменное событие
UserSecurityChangedEvent(видPasswordChanged); - отзыв всех прочих активных refresh-сессий пользователя, кроме текущей (INV-S7, причина
PasswordChanged).
- если пароль уже установлен:
- Что меняется. В одном согласованном изменении обновляется пользователь и отзываются прочие сессии; конкурентная смена пароля разрешается оптимистической блокировкой (правка не должна затирать чужую).
- События. Интеграционных нет (флаг
hasPasswordне входит в снапшотpin.identity.users.v1); доменноеUserSecurityChangedEvent— лента админ-аудита. - Результат.
investorId; пароли и хэши в ответ, логи и события не попадают; HTTP 200.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|---|
| Интеграционные события | none |
| Внешние вызовы | none |
| Проекции / поиск | none |
| Уведомления | уведомление «пароль изменён» через сервис notifications |
Отзываются все refresh-сессии пользователя, кроме текущей (INV-S7, причина PasswordChanged): на
прочих устройствах обновление токена больше не пройдёт. Уже выданные access-JWT на других устройствах
остаются валидными до истечения своего срока (короткоживущие) — мгновенного разлогина на них нет;
фактический выход происходит на ближайшем refresh. Текущая сессия сохраняется, поэтому клиент, сменивший
пароль, не разлогинивается.
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|---|---|---|---|
| — (auth) | 401 | Нет/просрочен JWT | клиент выполняет re-login | re-login |
ValidationError | 400 | newPassword вне 8–128 симв. | Список ошибок валидации | no retry |
IDENTITY_INVALID_CURRENT_PASSWORD | 400 | Неверный/отсутствующий currentPassword при установленном пароле (INV-6) | «Неверный текущий пароль» | no retry |
IDENTITY_USER_BLOCKED | 400 | Status != Active | «Учётная запись заблокирована» | no retry |
IDENTITY_USER_LOCKED_OUT | 400 | LockoutUntil > now (INV-8) | «Операция временно заблокирована» | retry позже |
Acceptance Criteria
- Given активный пользователь с установленным паролем и верным
currentPassword, When change-password с валиднымnewPassword(8–128), Then сохраняется новый хэш,PasswordChangedAt = now, счётчик неудачных попыток обнуляется, поднимаетсяUserSecurityChangedEvent(PasswordChanged), прочие refresh-сессии отзываются, ответ200 {investorId}. - Given пользователь без установленного пароля (первая установка), When change-password без
currentPassword, Then пароль устанавливается, дальнейший вход предлагает шагRequiresPassword. - Given пароль установлен,
currentPasswordневерен, When change-password, ThenIDENTITY_INVALID_CURRENT_PASSWORD(400), счётчик неудачных попыток увеличивается; на 10-й подряд — блокировка входа на 15 минут. - Given
LockoutUntil > now(INV-8), When change-password, ThenIDENTITY_USER_LOCKED_OUT(400) без сверки пароля. - Given
Status != Active, When change-password, ThenIDENTITY_USER_BLOCKED(400). - Given
newPasswordкороче 8 или длиннее 128, When change-password, ThenValidationError(400); значения паролей не попадают в логи/событие/ответ.
Совместимость и наблюдаемость
- Ужесточение парольной политики — только серверно (min 8 остаётся контрактным минимумом); additive-поля результата допустимы.
- Logs:
UserId, вид операции (set/change), результат — без значений паролей; metrics:identity_password_changes_total{kind,result}; traces — OTel.
Обратные ссылки
Автоссылки: где используется этот документ.
- API (2): GET /api/investors/me — Текущий инвестор (getCurrentInvestor), Identity API — карта областей и реестр investor-области
- registries (1): Реестр API-методов PIN
Связанные документы
- Сущность: user.md (INV-6/8,
PasswordHash,PasswordChangedAt). - Смежные: verify-sign-in-password.md (использование пароля во входе), user-session.md (INV-S7 отзыв сессий).