Files
bare/docs/decisions/031-relogin-service-logout.md
T
mayatnikovandClaude Opus 5 597c55301c Этап 1: аккаунты — argon2id, сессии, ключевой блоб, вход и регистрация
Сервер: миграция 001 со всей схемой storage.md, store на modernc.org/sqlite
(WAL, foreign_keys, один писатель), фоновая чистка раз в час, argon2id
с параметрами ADR-021 и сверкой constant-time, сессии по SHA-256 токена,
cookie bare_session, глобальная проверка Origin, девять эндпоинтов аккаунта.
Ник в журнал не попадает: для /api/ пишется шаблон маршрута.

Клиент: crypto.js по crypto.md построчно — мастер из пароля, два независимых
ключа из мастера, ключевой блоб с ником в AAD, отпечаток от сырой точки;
db.js со всеми хранилищами версии 1; экран входа и регистрации, настройки
со сменой пароля, выходом и удалением аккаунта.

Пароль не покидает клиент: проверено на боевом сервере — ни пароля, ни priv.d
ни в одном теле запроса, вход на втором устройстве даёт тот же отпечаток.

ADR-027: код internal для 500, причина только в журнале.
ADR-028: тексты состояний клиента сведены в ui.md.
ADR-029: вход под другим ником стирает историю только после подтверждения.
ADR-030: верхняя граница итераций KDF, проверка границ на обеих сторонах.
ADR-031: служебный выход перед повторным входом не заканчивает сеанс.
ADR-032: каталог состояния 0700, файлы базы 0600.

Прямые зависимости: modernc.org/sqlite, golang.org/x/crypto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-22 14:06:07 +03:00

4.6 KiB
Raw Blame History

ADR-031: Служебный выход перед повторным входом

Контекст

Ключевой блоб отдаёт только POST /api/login: GET /api/me его не возвращает, отдельного эндпоинта в docs/protocol.md нет. Поэтому смена пароля проходит через вход. Вход заводит новую сессию и перезаписывает cookie, а cookie — HttpOnly: прежний токен после этого недостижим, закрыть ту сессию клиенту уже нечем. Оставлять её живой нельзя — украденная cookie пережила бы смену пароля, ради которой всё и затевалось. Значит, выход идёт первым, до входа.

Но POST /api/logout — обычный непубличный запрос, и 401 unauthenticated на нём по docs/ui.md («Сеть и состояния») выбрасывает на экран входа. Отсюда отказ. Смена пароля с неверным старым паролем закрывает сессию и падает на входе; клиент остаётся на настройках и показывает «неверный пароль». Вторая попытка, уже с верным паролем, начинается с того же служебного выхода, получает 401 — и уходит на экран входа молча: ошибка пишется в узел, которого в документе уже нет. Удаление аккаунта после такой попытки получает 401 на DELETE /api/me и тоже уезжает на экран входа, ничего не удалив.

Решение

  • Смена пароля и удаление аккаунта начинаются со служебного выхода, потом входят заново. Порядок «выход → вход» не оставляет на сервере сессию, токена от которой нет ни у кого.
  • Служебный выход не заканчивает сеанс для пользователя. 401 unauthenticated на нём означает «сессии и так нет» и считается успехом: следующий шаг открывает новую. Обработчик истёкшей сессии на таком ответе не зовётся, ошибка не бросается. Прочие отказы — нет сети, 500 — поднимаются наверх и показываются как есть: при живой сессии входить заново нельзя.
  • Мимо обработчика идёт ровно этот вызов. 401 unauthenticated на любом другом запросе по-прежнему ведёт на экран входа с сохранением IndexedDB.
  • Удаление аккаунта входит прямым POST /api/login, без разбора блоба: аккаунт с испорченным блобом обязан удаляться.
  • Кнопка «выйти» пользуется тем же вызовом: сеанс там заканчивает сам клиент — стирает базу и рисует экран входа, — а не ответ сервера.

Следствия

  • Сорвавшаяся смена пароля оставляет клиент без сессии, но на своём экране и со своей строкой: «неверный пароль», «нет соединения». Следующая попытка — смена пароля или удаление аккаунта — начинается с того же служебного выхода и проходит целиком.
  • Перезагрузка страницы в этом состоянии показывает экран входа: GET /api/me отвечает 401, IndexedDB цела. Это обычный сценарий истёкшей сессии, отдельного обхождения не требует.
  • Сервер не меняется: POST /api/logout и POST /api/login работают как записано в docs/protocol.md.
  • В docs/ui.md правило уточняется до 401 unauthenticated: 401 invalid_credentials — ошибка формы, на экран входа она не выбрасывала и раньше.