Files
specs/openspec/specs/user-authentication/spec.md
T
codex-bot 1bd6181585
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
spec: align user-authentication password requirement with ASCII 8-72 policy (WealthLedger/specs#46) (#47)
2026-10-16 10:47:41 +03:00

195 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# user-authentication Specification
## Purpose
Позволяет пользователю создать и восстановить личную сессию, чтобы его данные и будущие подключения к брокеру были доступны только ему.
## Requirements
### Requirement: Регистрация пользователя
Система SHALL позволять зарегистрировать User по уникальному нормализованному email и паролю. Email MUST нормализоваться удалением внешних пробелов и приведением к нижнему регистру; он MUST содержать не более 254 символов, ровно один `@`, непустые local и domain части и не содержать пробельных или управляющих символов. Пароль MUST передаваться без изменения (без trim и нормализации) и состоять из 8–72 печатных ASCII-символов U+0020–U+007E (латинские буквы, цифры, спецсимволы и пробел); другие символы, включая не-ASCII и управляющие, MUST отвергаться. Требований к составу символов (обязательных классов) нет. Успешная регистрация MUST создать первую Session и вернуть профиль `{ id, email }` и access token.
#### Scenario: Успешная регистрация
- **WHEN** пользователь отправляет новый email и валидный пароль
- **THEN** система создаёт User и Session, устанавливает refresh-cookie и возвращает access token с нормализованным профилем
#### Scenario: Нормализация email при регистрации
- **WHEN** пользователь регистрирует ` Anna@Example.com `
- **THEN** система сохраняет и возвращает email `anna@example.com`
#### Scenario: Повторная регистрация
- **WHEN** пользователь регистрирует email, уже принадлежащий User после нормализации
- **THEN** система отвечает `409` Problem Details с code `EMAIL_ALREADY_REGISTERED`
#### Scenario: Конкурентная регистрация
- **WHEN** два запроса одновременно регистрируют один нормализованный email
- **THEN** ровно один запрос создаёт User и Session, а второй отвечает `409` Problem Details с code `EMAIL_ALREADY_REGISTERED`
#### Scenario: Невалидные регистрационные данные
- **WHEN** пользователь отправляет email или пароль, не соответствующий правилам регистрации
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателями на невалидные поля
#### Scenario: Границы длины пароля при регистрации
- **WHEN** пользователь регистрируется с паролем из 7, 8, 72 или 73 печатных ASCII-символов
- **THEN** пароли из 8 и 72 символов принимаются, а из 7 и 73 отвергаются `400` Problem Details с code `VALIDATION_ERROR` и указателем `#/password`
#### Scenario: Недопустимые символы пароля при регистрации
- **WHEN** пользователь регистрирует пароль с кириллицей, emoji, табуляцией, переводом строки или DEL
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателем `#/password`, не создавая User
#### Scenario: Пробелы пароля не изменяются
- **WHEN** пользователь регистрирует пароль с пробелами в начале или конце
- **THEN** система принимает пароль как есть, а вход требует тот же пароль с теми же пробелами
### Requirement: Вход пользователя
Система SHALL позволять User начать новую независимую Session по нормализованному email и паролю. Успешный вход MUST вернуть `{ id, email }`, access token и установить refresh-cookie. Неверный пароль и неизвестный email MUST иметь одинаковый ответ, не раскрывающий наличие User. Пароль при входе MUST быть непустой строкой не длиннее 72 UTF-8 байтов; минимальная длина и набор символов при входе MUST NOT проверяться, чтобы User, зарегистрированный по прежним правилам (в том числе с не-ASCII паролем), мог войти.
#### Scenario: Успешный вход
- **WHEN** пользователь отправляет зарегистрированный email и правильный пароль
- **THEN** система создаёт новую Session, устанавливает refresh-cookie и возвращает access token с профилем
#### Scenario: Неверные учётные данные
- **WHEN** пользователь отправляет неизвестный email или неверный пароль
- **THEN** система отвечает `401` Problem Details с code `INVALID_CREDENTIALS`
#### Scenario: Вход с прежним паролем
- **WHEN** User, зарегистрированный по прежним правилам с не-ASCII паролем, отправляет правильный пароль
- **THEN** система выполняет вход, не применяя к паролю правила регистрации
#### Scenario: Пароль при входе длиннее 72 байтов
- **WHEN** пользователь отправляет пароль длиннее 72 UTF-8 байтов
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателем `#/password`
### Requirement: Access и refresh сессии
Система SHALL выдавать access token сроком 15 минут и refresh token сроком 7 дней. Access token MUST передаваться клиенту только в ответе API и использоваться как Bearer token. Refresh token MUST передаваться только в `httpOnly`, `SameSite=Lax`, host-only cookie `refresh_token` с path `/api/v1/auth`; в HTTPS-среде cookie MUST иметь атрибут `Secure`. Каждый новый refresh token MUST иметь уникальный JWT ID и ротироваться при успешном обновлении Session; Session MUST оставаться активной 7 дней после последнего успешного обновления. Одновременные Session одного User MUST быть независимы и не иметь числового лимита.
#### Scenario: Успешное восстановление сессии
- **WHEN** клиент отправляет действительный refresh-cookie
- **THEN** система заменяет refresh-cookie и возвращает новый access token и профиль Session
#### Scenario: Конкурентное восстановление сессии
- **WHEN** два запроса одновременно отправляют один действительный refresh-cookie одной Session
- **THEN** ровно один запрос заменяет refresh-cookie и возвращает новый access token и профиль, второй отвечает `401` Problem Details с code `UNAUTHORIZED`, а Session победителя остаётся активной
#### Scenario: Истёкшая сессия
- **WHEN** клиент отправляет refresh-cookie Session, не обновлявшейся более 7 дней
- **THEN** система отвечает `401` Problem Details с code `UNAUTHORIZED`, не изменяя cookie неуспешным refresh
#### Scenario: Повторное использование refresh token
- **WHEN** клиент отправляет refresh token, заменённый предыдущим успешным refresh
- **THEN** система отвечает `401` Problem Details с code `UNAUTHORIZED`, не изменяет cookie и не отзывает связанную Session только из-за этого запроса
### Requirement: Межвкладочная координация не требуется
Frontend MUST сохранять изоляцию данных текущей identity внутри вкладки.
Он MUST NOT требовать cross-tab synchronization для auth flow. После смены
auth-состояния в другой вкладке допускаются reload или повторная авторизация.
Неуспешный refresh MUST NOT удалять cookie успешного конкурентного запроса.
#### Scenario: Конкурентный refresh проигрывает в другой вкладке
- **WHEN** вкладка получает `401` после ротации общей cookie другой вкладкой
- **THEN** она может перейти на login; cookie победителя остаётся доступной для последующего восстановления
### Requirement: Завершение сессии
Система SHALL позволять завершить текущую Session через logout. Logout MUST отозвать Session, если refresh-cookie соответствует активной Session, и MUST удалить refresh-cookie. Logout MUST быть идемпотентным и не раскрывать существование Session.
#### Scenario: Logout действующей сессии
- **WHEN** клиент вызывает logout с действительным refresh-cookie
- **THEN** система отзывает текущую Session, удаляет refresh-cookie и отвечает `204`
#### Scenario: Logout без refresh-cookie
- **WHEN** клиент вызывает logout без refresh-cookie или с невалидным refresh-cookie
- **THEN** система удаляет refresh-cookie и отвечает `204`
### Requirement: Доступ к текущему пользователю и защищённым API
Система SHALL предоставлять `GET /api/v1/auth/me`, возвращающий `{ id, email }` текущего User. Этот маршрут и все прикладные API вне `/api/v1/auth` MUST требовать действительный неистёкший access token. Refresh token MUST NOT предоставлять доступ к защищённому API. Logout и refresh MUST NOT требовать access token.
#### Scenario: Получение текущего профиля
- **WHEN** User вызывает `GET /api/v1/auth/me` с действительным access token
- **THEN** система возвращает `{ id, email }` этого User
#### Scenario: Запрос без access token
- **WHEN** клиент вызывает защищённый API без действительного access token
- **THEN** система отвечает `401` Problem Details с code `UNAUTHORIZED`
#### Scenario: Refresh token как Bearer token
- **WHEN** клиент передаёт refresh token в Authorization header защищённого API
- **THEN** система отвечает `401` Problem Details с code `UNAUTHORIZED`
### Requirement: Стандартные ответы об ошибках
Система SHALL возвращать все API-ошибки как `application/problem+json`. Каждый Problem Details MUST содержать поле `status` и extension `code`. Ошибки валидации MUST использовать `400` и code `VALIDATION_ERROR`, а также extension `errors` как массив объектов `{ pointer, detail }`, где pointer является JSON Pointer поля запроса. Ошибки, не требующие специальной обработки клиентом, MUST использовать один из code `BAD_REQUEST`, `NOT_FOUND`, `UNSUPPORTED_MEDIA_TYPE` или `INTERNAL_SERVER_ERROR` согласно HTTP status.
#### Scenario: Ошибка DTO-валидации
- **WHEN** API получает тело запроса, не проходящее валидацию
- **THEN** API возвращает `application/problem+json` с `status` 400, code `VALIDATION_ERROR` и `errors` для невалидных полей
#### Scenario: Внутренняя ошибка
- **WHEN** API сталкивается с необработанной внутренней ошибкой
- **THEN** API возвращает `application/problem+json` с `status` 500 и code `INTERNAL_SERVER_ERROR` без внутренних деталей
### Requirement: Клиентский поток аутентификации
Клиент SHALL предоставлять формы входа и регистрации. Обе формы MUST валидировать email и пароль до отправки; форма регистрации MUST дополнительно требовать совпадающее подтверждение пароля, не передавая его API. Все поля, ссылки и submit MUST быть отключены во время отправки. Клиент MUST хранить access token и профиль только в памяти, восстанавливать их вызовом refresh при старте и передавать cookie при API-вызовах.
#### Scenario: Несовпадающее подтверждение пароля
- **WHEN** пользователь отправляет регистрацию с несовпадающим подтверждением пароля
- **THEN** клиент показывает ошибку поля и не вызывает API
#### Scenario: Восстановление после перезагрузки
- **WHEN** приложение запускается с действительным refresh-cookie
- **THEN** клиент получает новый access token и профиль до отображения защищённого маршрута
#### Scenario: Сбой отправки формы
- **WHEN** login или registration API недоступен или возвращает неожиданную ошибку
- **THEN** клиент завершает pending-состояние, сохраняет введённые поля и показывает локализованную общую ошибку
### Requirement: Защищённая клиентская навигация
Клиент SHALL ограничивать все маршруты, кроме `/login` и `/register`, активной access-сессией. Неавторизованного пользователя MUST перенаправлять на login и после успешной аутентификации MUST возвращать на исходный внутренний pathname и search либо на `/accounts`. Авторизованного пользователя на auth-маршруте MUST перенаправлять на `/accounts`. При `UNAUTHORIZED` от защищённого API клиент MUST выполнить один сериализованный refresh, повторить исходный запрос один раз и при неуспехе очистить сессию и cache перед переходом на login.
#### Scenario: Возврат на исходный маршрут
- **WHEN** неавторизованный пользователь открывает `/accounts/123?tab=positions` и затем успешно входит
- **THEN** клиент направляет его на `/accounts/123?tab=positions`
#### Scenario: Потеря сессии при защищённом запросе
- **WHEN** защищённый API отвечает `401` с code `UNAUTHORIZED` и refresh не восстанавливает сессию
- **THEN** клиент очищает access token, профиль и cache, затем направляет пользователя на `/login`
#### Scenario: Выход из настроек
- **WHEN** пользователь нажимает logout на `/settings`
- **THEN** клиент очищает локальную сессию и cache и направляет пользователя на `/login` независимо от результата сетевого запроса
### Requirement: Единый HTTP contract успешных auth endpoints
Успешные `POST /api/v1/auth/register`, `POST /api/v1/auth/login` и
`POST /api/v1/auth/refresh` MUST отвечать HTTP `200`; опубликованный
OpenAPI-документ, зафиксированный frontend snapshot и сгенерированные
transport types MUST описывать этот же status. Каждая операция сохраняет свои
существующие payload, refresh-cookie и session semantics. Logout MUST
продолжать отвечать `204`.
#### Scenario: Успешная регистрация возвращает контрактный status
- **WHEN** пользователь отправляет новую валидную регистрацию
- **THEN** система устанавливает refresh-cookie, возвращает профиль и access
token с HTTP `200`
#### Scenario: Успешный login или refresh возвращает контрактный status
- **WHEN** пользователь успешно входит либо восстанавливает Session
- **THEN** система устанавливает или заменяет refresh-cookie, возвращает
профиль и access token с HTTP `200`
### Requirement: Клиент контролируемо завершает неуспешный refresh
Frontend MUST обрабатывать HTTP и транспортную ошибку общего refresh flow без
необработанного отклонения Promise. При неуспешном cold-start refresh frontend
MUST завершить инициализацию как неавторизованный, не сохраняя access token
или профиль. При неуспешном refresh после защищённого запроса frontend MUST
очистить in-memory session и query cache перед existing navigation to login.
#### Scenario: Cold-start refresh завершается транспортной ошибкой
- **WHEN** приложение запускается без access token и refresh запрос не может
получить транспортный response
- **THEN** frontend завершается в готовом неавторизованном состоянии без
необработанного отклонения Promise и показывает обычный public/login flow
#### Scenario: Повторный refresh после protected 401 завершается транспортной ошибкой
- **WHEN** защищённый API отвечает `401`, а единственный serialised refresh
завершается транспортной ошибкой
- **THEN** frontend очищает in-memory session и query cache и направляет User
в существующий login flow без необработанного отклонения Promise