Files
progcode/editorial/agent-rewrites/176.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
14 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 176,
"slug": "editorial-2023-02-mechanism-sessions-auth",
"title": "Сессия после rotation и logout: кто решает, действителен ли запрос",
"excerpt": "Cookie переносит идентификатор, но не принимает решение о доступе. Разбираем серверную запись сессии, смену current ID, logout и проверяемый отрицательный путь со старым идентификатором.",
"contentHtml": "<p>Пользователь нажимает «Сохранить» после logout в другой вкладке. Интерфейс показывает успешный ответ, хотя сервер уже должен был закрыть сессию. В другом варианте после rotation старый запрос получает новый доступ, потому что обработчик проверяет только наличие cookie. Цена ошибки — изменение данных после отзыва доступа, потеря новой сессии поздней операцией со старым ID и расследование, в котором нельзя назвать источник истины.</p>\n<p>Тезис статьи прост: cookie доставляет непрозрачный ID, а серверная запись принимает решение. Сервер хранит статус записи, связь между версиями сессии и current ID. Rotation заменяет current ID и делает старый ID недействительным. Logout отзывает запись и очищает cookie с тем же scope. Ни один флаг cookie не заменяет эти проверки.</p>\n<figure><img src=\"/assets/editorial/2023/sessions-auth-2023-cookie-contract.svg\" alt=\"Контракт учебной cookie __Host-session и серверной проверки active status\" loading=\"lazy\" /><figcaption>Учебная схема разделяет доставку cookie и решение сервера. Она не показывает настоящий браузерный trace, reverse proxy или достаточность защиты от CSRF.</figcaption></figure>\n<h2>Четыре факта, которые нельзя склеивать</h2>\n<p>Аутентификация отвечает на вопрос «кто прошёл вход?». В этой модели она не представлена. Сервис может получать identity из другого механизма, но затем всё равно проверяет сессию.</p>\n<p>Cookie delivery отвечает на другой вопрос: какой ID браузер приложил к запросу. Имя, домен, путь, Secure и SameSite влияют на доставку. Наличие cookie не доказывает, что запись существует, не отозвана и относится к текущей версии.</p>\n<p>Server record хранит status, срок и поколение. Обработчик находит запись, проверяет active status, expiry и current ID, а потом передаёт контекст в authorization. Authorization отдельно решает, может ли identity выполнить конкретное действие.</p>\n<table><caption>Слои сессии и границы решения</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Вопрос</th><th scope=\"col\">Проверка</th><th scope=\"col\">Чего он не доказывает</th></tr></thead><tbody><tr><td>Cookie delivery</td><td>Какой ID пришёл?</td><td>Заголовок и scope</td><td>Что ID действителен</td></tr><tr><td>Server record</td><td>Принимать ли ID?</td><td>status, expiry, current</td><td>Право на действие</td></tr><tr><td>Lineage</td><td>Какой ID заменил старый?</td><td>generation или successor</td><td>Атомарность двух запросов</td></tr><tr><td>Authorization</td><td>Можно ли выполнить операцию?</td><td>роль, ресурс, действие</td><td>Безопасность cookie</td></tr></tbody></table>\n<h2>Границы флагов cookie</h2>\n<p><code>Secure</code> ограничивает отправку по защищённому каналу. Он не шифрует запись сессии и не отзывает её. <code>HttpOnly</code> скрывает значение от обычного JavaScript API. Он уменьшает риск кражи через клиентский код, но не устраняет XSS и не запрещает браузеру приложить cookie.</p>\n<p><code>SameSite=Lax</code> ограничивает часть cross-site отправок. Это слой защиты, а не замена CSRF-проверке mutation endpoint. Сценарии с embed, federation и несколькими доменами требуют отдельного решения.</p>\n<p><code>Max-Age</code> и <code>Expires</code> задают срок хранения в user agent. Браузер может удалить cookie раньше. Сервер не должен принимать ID только потому, что cookie пришла, и не должен считать удаление cookie доказательством logout. Browser lifetime и server expiry — разные часы.</p>\n<p>Префикс <code>__Host-</code> подходит для cookie одного host: нужны <code>Secure</code>, <code>Path=/</code> и отсутствие <code>Domain</code>. Это не граница авторизации. Административный маршрут всё равно проверяет права на сервере. Для нескольких поддоменов нужен другой scope и явное описание его владельца.</p>\n<h2>Rotation заменяет current ID</h2>\n<p>Rotation меняет идентификатор, который сервер считает текущим. Операция переводит старую запись в <code>rotated</code>, создаёт successor и передвигает указатель current. Старый ID не получает новый TTL. Он возвращает отказ вроде <code>stale-session</code>.</p>\n<pre><code>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));</code></pre>\n<p>Это учебный фрагмент. Он показывает порядок проверки и условие compare-and-swap, но не готовый adapter для конкретной базы. В реальном хранилище нужно определить транзакцию, уникальность successor и поведение повторной доставки.</p>\n<p>Без линейзации два запроса могут прочитать один active ID и выпустить двух successor. Нельзя лечить эту гонку увеличением TTL. Нужен атомарный переход от ожидаемого поколения.</p>\n<h2>Logout отзывает серверную запись</h2>\n<p>Logout-current отзывает одну текущую запись. Logout-lineage отзывает цепочку одного входа. Logout-all отзывает все записи identity. Это разные операции. Endpoint должен назвать scope. Иначе поздний logout старой вкладки выключит новую сессию или оставит действующий successor.</p>\n<p>После отзыва сервер очищает cookie с тем же именем, доменом и путём. Это улучшает UX и уменьшает повторные запросы. Источником истины остаётся status серверной записи.</p>\n<pre><code>const 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();</code></pre>\n<p>Старый ID после rotation не должен отзывать successor, если выбран logout-current. Это отрицательный путь. Happy path с текущим ID его не проверяет.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><caption>Диагностика рассинхронизации cookie и серверной записи</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Cookie есть, но ответ 401</td><td>Запись revoked, rotated или expired</td><td>Сопоставить ID, status и current</td><td>Вернуть единый reject и очистить cookie</td></tr><tr><td>Старый запрос изменил данные</td><td>Проверили наличие ID, но не status/current</td><td>Повторить запрос после rotation</td><td>Проверять запись до эффекта</td></tr><tr><td>Старая вкладка выключила новую</td><td>Logout отзывает всю lineage</td><td>Сопоставить ID и scope в логе</td><td>Явно выбрать logout-current или logout-all</td></tr><tr><td>Стали действующими два ID</td><td>Rotation не линеаризована</td><td>Отправить два запроса одного generation</td><td>Добавить транзакцию или conditional update</td></tr><tr><td>Прошла cross-site mutation</td><td>SameSite принят за полную CSRF-защиту</td><td>Проверить Origin и CSRF-контракт</td><td>Добавить отдельную серверную проверку</td></tr></tbody></table>\n<h2>Порядок проверки</h2>\n<ol><li>Назовите защищённый handler и его действие.</li><li>Опишите владельца browser delivery, server record, lineage и authorization.</li><li>Зафиксируйте active, rotated, revoked и expired и ответ для каждого состояния.</li><li>Проверьте вход, запрос с current ID и успешную rotation.</li><li>Проверьте старый ID после rotation: он получает reject и не меняет successor.</li><li>Проверьте выбранный logout scope. Старый logout не меняет новую сессию при logout-current.</li><li>Проверьте Set-Cookie и очистку в разрешённом интеграционном окружении. Unit fixture не доказывает поведение браузера.</li><li>Сопоставьте active session с отдельной authorization-проверкой.</li></ol>\n<h2>Ограничения модели</h2>\n<p>Пример не генерирует секреты, не читает cookie jar и не отправляет HTTP. Имена <code>fixture-s-1</code>, generation и фиксированный TTL учебные. Их нельзя копировать как production ID. Модель не покрывает распределённые блокировки, clock skew, несколько устройств, CORS, CSRF policy, reauthentication и reverse proxy.</p>\n<p>Документы IETF и NIST описывают протокол и термины, но не доказывают корректность конкретной платформы. Browser test не доказывает атомарность базы. Storage test не доказывает scope Set-Cookie. Эти границы проверяют отдельно.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Механизм готов к интеграционной проверке, если для одного handler видны четыре результата: current ID проходит до authorization; rotated ID получает reject без изменения successor; выбранный logout отзывает ровно ожидаемые записи; сервер принимает решение независимо от наличия cookie. В отчёте есть correlation ID, lineage, generation и причина отказа, но нет самого секрета.</p>\n<p>Если один результат нельзя показать отдельно, контракт ещё не определён. Сначала фиксируют владельца перехода и состояние, затем выбирают хранилище и браузерный сценарий.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://datatracker.ietf.org/doc/html/rfc6265\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 6265: HTTP State Management Mechanism</a></li><li><a href=\"https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-11\" target=\"_blank\" rel=\"noopener noreferrer\">IETF draft-ietf-httpbis-rfc6265bis-11: Cookies</a></li><li><a href=\"https://pages.nist.gov/800-63-3/sp800-63b.html\" target=\"_blank\" rel=\"noopener noreferrer\">NIST SP 800-63B: Digital Identity Guidelines, Authentication and Lifecycle Management</a></li></ul>"
}