130 lines
9.9 KiB
Markdown
130 lines
9.9 KiB
Markdown
# Аутентификация и сессии
|
||
|
||
Аутентификация связывает локального [пользователя](../../glossary.md#user)
|
||
с его сессией WealthLedger. Она открывает доступ к приложению; доступ
|
||
к брокерским данным определяется отдельным
|
||
[подключением T-Bank](../../integrations/tbank.md). Backend проверяет
|
||
учётные данные и управляет сессиями, frontend восстанавливает состояние
|
||
входа и выбирает доступные маршруты.
|
||
|
||
## Регистрация и вход
|
||
|
||
Пользователь регистрируется или входит по email и паролю. Регистрация сразу
|
||
создаёт первую сессию. Внешние пробелы email удаляются, буквы приводятся
|
||
к нижнему регистру.
|
||
|
||
Пароль при регистрации — от 8 до 72 печатных ASCII-символов: латинские
|
||
буквы, цифры, спецсимволы и пробел. Кириллица, emoji, другие не-ASCII и
|
||
управляющие символы отклоняются. Пароль не обрезается и не нормализуется, а
|
||
обязательных классов символов нет. Верхняя граница вытекает из bcrypt, который
|
||
учитывает только первые 72 байта: для ASCII символ равен байту, поэтому
|
||
длинный пароль не обрезается молча. Frontend и backend проверяют одну и ту же
|
||
политику.
|
||
|
||
При входе проверяется только то, что пароль непустой и не длиннее 72 UTF-8
|
||
байтов. Минимум и набор символов не применяются, чтобы пользователь,
|
||
зарегистрированный по прежним правилам (в том числе с не-ASCII паролем), мог
|
||
войти. Невалидный пароль получает `400` с кодом `VALIDATION_ERROR`.
|
||
|
||
Access token действует 15 минут и хранится только в памяти браузера.
|
||
Refresh token имеет серверный срок 7 дней, заменяется при восстановлении
|
||
сессии и передаётся в cookie `refresh_token` с `httpOnly` и `SameSite=Lax`.
|
||
Сроки определены в [спецификации сессий](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/user-authentication/spec.md#requirement-access-и-refresh-сессии)
|
||
и [ADR 006](../../adr/006-auth-session-policy.md).
|
||
|
||
После перезагрузки SPA получает новый access token через refresh-cookie.
|
||
Пока результат неизвестен, пользователь видит нейтральный skeleton каркаса
|
||
без преждевременного показа public или protected содержимого. После
|
||
завершения восстановления приложение выбирает доступный маршрут.
|
||
|
||
Все маршруты, кроме `/login` и `/register`, требуют сессию. После входа
|
||
пользователь возвращается на исходный внутренний маршрут, если он был.
|
||
Logout доступен в настройках: frontend очищает локальную сессию независимо
|
||
от состояния сети, а backend отзывает серверную сессию при предъявлении
|
||
её действительного refresh-cookie.
|
||
|
||
## Восстановление сессии
|
||
|
||
Динамическая схема показывает восстановление после reload между SPA, auth API
|
||
и локальным хранилищем сессий. SQLite здесь — файл базы backend.
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant F as React SPA
|
||
participant B as Backend API
|
||
participant D as Локальная SQLite
|
||
Note over F: Reload: access token в памяти отсутствует
|
||
F->>B: Refresh с httpOnly cookie
|
||
B->>D: Проверить Session и текущий hash token
|
||
alt Действительная сессия, ротация выиграна
|
||
B->>D: Атомарно заменить hash и срок
|
||
B-->>F: Access token, профиль, новая refresh-cookie
|
||
F->>B: Protected request с Bearer
|
||
else Cookie нет или refresh отклонён
|
||
B-->>F: 401 без изменения cookie
|
||
Note over F: Готовое неавторизованное состояние
|
||
end
|
||
```
|
||
|
||
В одной вкладке одновременные refresh используют один незавершённый запрос;
|
||
cookie-writing requests упорядочены локально. Межвкладочная синхронизация
|
||
не поддерживается: после смены сессии в другой вкладке допустимы reload/re-login.
|
||
Backend защищает ротацию атомарно. Неуспешный refresh не удаляет cookie
|
||
победившего запроса; недействительный token по-прежнему не восстанавливает сессию.
|
||
|
||
Обычный защищённый запрос с `401 UNAUTHORIZED` проходит refresh и один повтор
|
||
через общий транспорт; auth endpoints из этого цикла исключены. Само
|
||
истечение access token не запускает фоновый таймер refresh.
|
||
|
||
## HTTP-граница
|
||
|
||
Endpoints, статусы и формы публичных ответов определяет backend OpenAPI;
|
||
граница производного клиента описана в
|
||
[HTTP API](../../architecture/http-api.md#источник-контракта).
|
||
Refresh-cookie ограничена auth API. Признак Secure зависит от режима
|
||
окружения: в development допускается локальный HTTP, в остальных режимах
|
||
Secure-cookie требует защищённого соединения.
|
||
|
||
HTTP-ошибка и транспортная ошибка refresh одинаково завершают начальное
|
||
восстановление готовым неавторизованным состоянием. При refresh после
|
||
защищённого `401` frontend очищает сессию в памяти и query cache, сохраняя
|
||
исходный `401` для перехода ко входу. Общая форма ошибок и известное
|
||
расхождение деталей валидации находятся в
|
||
[HTTP API](../../architecture/http-api.md#error-contract).
|
||
|
||
## Ограничения
|
||
|
||
Описанный auth flow ограничен регистрацией, входом, восстановлением сессии
|
||
и logout. Подтверждение email, восстановление или смена пароля, смена email,
|
||
удаление аккаунта, список устройств, logout всех устройств и rate limit
|
||
попыток входа не входят в эту реализованную возможность. Access token
|
||
может оставаться действительным до 15 минут после logout.
|
||
|
||
Если несколько клиентов одновременно предъявляют один действительный
|
||
refresh token, сервер завершает одну ротацию успешно, остальные запросы
|
||
получают `401`. Сессия победившего запроса остаётся действительной. Уже
|
||
заменённый refresh token также даёт `401`, но сам по себе не отзывает сессию:
|
||
механизм не хранит историю token или grace-window, чтобы отличить
|
||
конкурентный запрос от replay. Это ограничение и его причины сохранены
|
||
в [ADR 009](../../adr/009-concurrent-refresh-rotation-policy.md).
|
||
|
||
Refresh-cookie не задаёт собственные `maxAge` или `expires`.
|
||
Семидневный серверный срок refresh token не является обещанием сохранения
|
||
входа после закрытия браузера.
|
||
|
||
## Ответственность и проверка
|
||
|
||
Backend проверяет учётные данные и tokens, хранит состояние Session и
|
||
атомарно выполняет ротацию. Frontend управляет состоянием входа,
|
||
восстановлением после reload и доступностью экранов; общий HTTP-транспорт
|
||
передаёт Bearer token и выполняет описанный refresh/retry.
|
||
|
||
Проверки конкурентной ротации, браузерного состояния и транспорта охватывают
|
||
разные границы. Их изолированная проверка не заменяет полный HTTP-поток
|
||
и проверку атомарности с реальной изолированной базой.
|
||
|
||
Согласованное поведение находится в workspace-спецификации
|
||
[User authentication](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/user-authentication/spec.md).
|
||
Правила начального ожидания — в
|
||
[состояниях интерфейса](../../architecture/cross-cutting/ui-resilience.md).
|