{ "index": 176, "slug": "editorial-2023-02-mechanism-sessions-auth", "title": "Сессия после rotation и logout: кто решает, действителен ли запрос", "excerpt": "Cookie переносит идентификатор, но не принимает решение о доступе. Разбираем серверную запись сессии, смену current ID, logout и проверяемый отрицательный путь со старым идентификатором.", "contentHtml": "
Пользователь нажимает «Сохранить» после logout в другой вкладке. Интерфейс показывает успешный ответ, хотя сервер уже должен был закрыть сессию. В другом варианте после rotation старый запрос получает новый доступ, потому что обработчик проверяет только наличие cookie. Цена ошибки — изменение данных после отзыва доступа, потеря новой сессии поздней операцией со старым ID и расследование, в котором нельзя назвать источник истины.
\nТезис статьи прост: cookie доставляет непрозрачный ID, а серверная запись принимает решение. Сервер хранит статус записи, связь между версиями сессии и current ID. Rotation заменяет current ID и делает старый ID недействительным. Logout отзывает запись и очищает cookie с тем же scope. Ни один флаг cookie не заменяет эти проверки.
\nАутентификация отвечает на вопрос «кто прошёл вход?». В этой модели она не представлена. Сервис может получать identity из другого механизма, но затем всё равно проверяет сессию.
\nCookie delivery отвечает на другой вопрос: какой ID браузер приложил к запросу. Имя, домен, путь, Secure и SameSite влияют на доставку. Наличие cookie не доказывает, что запись существует, не отозвана и относится к текущей версии.
\nServer record хранит status, срок и поколение. Обработчик находит запись, проверяет active status, expiry и current ID, а потом передаёт контекст в authorization. Authorization отдельно решает, может ли identity выполнить конкретное действие.
\n| Слой | Вопрос | Проверка | Чего он не доказывает |
|---|---|---|---|
| Cookie delivery | Какой ID пришёл? | Заголовок и scope | Что ID действителен |
| Server record | Принимать ли ID? | status, expiry, current | Право на действие |
| Lineage | Какой ID заменил старый? | generation или successor | Атомарность двух запросов |
| Authorization | Можно ли выполнить операцию? | роль, ресурс, действие | Безопасность cookie |
Secure ограничивает отправку по защищённому каналу. Он не шифрует запись сессии и не отзывает её. HttpOnly скрывает значение от обычного JavaScript API. Он уменьшает риск кражи через клиентский код, но не устраняет XSS и не запрещает браузеру приложить cookie.
SameSite=Lax ограничивает часть cross-site отправок. Это слой защиты, а не замена CSRF-проверке mutation endpoint. Сценарии с embed, federation и несколькими доменами требуют отдельного решения.
Max-Age и Expires задают срок хранения в user agent. Браузер может удалить cookie раньше. Сервер не должен принимать ID только потому, что cookie пришла, и не должен считать удаление cookie доказательством logout. Browser lifetime и server expiry — разные часы.
Префикс __Host- подходит для cookie одного host: нужны Secure, Path=/ и отсутствие Domain. Это не граница авторизации. Административный маршрут всё равно проверяет права на сервере. Для нескольких поддоменов нужен другой scope и явное описание его владельца.
Rotation меняет идентификатор, который сервер считает текущим. Операция переводит старую запись в rotated, создаёт successor и передвигает указатель current. Старый ID не получает новый TTL. Он возвращает отказ вроде stale-session.
const current = sessions.findById(request.cookies[SESSION_NAME]);\n\nif (!current || current.status !== 'active' || current.id !== lineage.currentId) {\n return response.status(401).json({ error: 'stale-session' });\n}\n\nconst next = sessions.rotateAtomically({ oldId: current.id, expectedGeneration: current.generation });\nresponse.setHeader('Set-Cookie', serializeSessionCookie(next.id));\nЭто учебный фрагмент. Он показывает порядок проверки и условие compare-and-swap, но не готовый adapter для конкретной базы. В реальном хранилище нужно определить транзакцию, уникальность successor и поведение повторной доставки.
\nБез линейзации два запроса могут прочитать один active ID и выпустить двух successor. Нельзя лечить эту гонку увеличением TTL. Нужен атомарный переход от ожидаемого поколения.
\nLogout-current отзывает одну текущую запись. Logout-lineage отзывает цепочку одного входа. Logout-all отзывает все записи identity. Это разные операции. Endpoint должен назвать scope. Иначе поздний logout старой вкладки выключит новую сессию или оставит действующий successor.
\nПосле отзыва сервер очищает cookie с тем же именем, доменом и путём. Это улучшает UX и уменьшает повторные запросы. Источником истины остаётся status серверной записи.
\nconst result = logoutCurrent({ presentedId: request.cookies[SESSION_NAME], lineageId: request.sessionLineage });\nif (result.kind === 'stale-session') return clearCookie(response).status(401).end();\nsessions.revoke(result.currentId);\nreturn clearCookie(response).status(204).end();\nСтарый ID после rotation не должен отзывать successor, если выбран logout-current. Это отрицательный путь. Happy path с текущим ID его не проверяет.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Cookie есть, но ответ 401 | Запись revoked, rotated или expired | Сопоставить ID, status и current | Вернуть единый reject и очистить cookie |
| Старый запрос изменил данные | Проверили наличие ID, но не status/current | Повторить запрос после rotation | Проверять запись до эффекта |
| Старая вкладка выключила новую | Logout отзывает всю lineage | Сопоставить ID и scope в логе | Явно выбрать logout-current или logout-all |
| Стали действующими два ID | Rotation не линеаризована | Отправить два запроса одного generation | Добавить транзакцию или conditional update |
| Прошла cross-site mutation | SameSite принят за полную CSRF-защиту | Проверить Origin и CSRF-контракт | Добавить отдельную серверную проверку |
Пример не генерирует секреты, не читает cookie jar и не отправляет HTTP. Имена fixture-s-1, generation и фиксированный TTL учебные. Их нельзя копировать как production ID. Модель не покрывает распределённые блокировки, clock skew, несколько устройств, CORS, CSRF policy, reauthentication и reverse proxy.
Документы IETF и NIST описывают протокол и термины, но не доказывают корректность конкретной платформы. Browser test не доказывает атомарность базы. Storage test не доказывает scope Set-Cookie. Эти границы проверяют отдельно.
\nМеханизм готов к интеграционной проверке, если для одного handler видны четыре результата: current ID проходит до authorization; rotated ID получает reject без изменения successor; выбранный logout отзывает ровно ожидаемые записи; сервер принимает решение независимо от наличия cookie. В отчёте есть correlation ID, lineage, generation и причина отказа, но нет самого секрета.
\nЕсли один результат нельзя показать отдельно, контракт ещё не определён. Сначала фиксируют владельца перехода и состояние, затем выбирают хранилище и браузерный сценарий.
\n