195 lines
20 KiB
Markdown
195 lines
20 KiB
Markdown
# 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
|