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

20 KiB
Raw Blame History

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
  • 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