Files
progcode/editorial/agent-rewrites/177.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
18 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": 177,
"slug": "editorial-2023-02-practice-sessions-auth",
"title": "Сессия после rotation и logout: один контракт для cookie и сервера",
"excerpt": "Как отделить cookie от серверной сессии, не принять старый ID после rotation и доказать logout проверкой доступа, а не только очисткой браузера.",
"contentHtml": "<p>Пользователь нажимает «Сохранить» после logout в другой вкладке. Интерфейс получает успешный ответ, хотя сессию уже должны были закрыть. В другом варианте старый запрос приходит после rotation и снова проходит, потому что обработчик проверяет только наличие cookie. Ошибка выглядит случайной. Цена ошибки измерима: сервер меняет данные после отзыва доступа, новая сессия может исчезнуть из-за позднего ответа, а расследование не знает, какой ID считался действующим.</p>\\\n<p>Тезис простой: cookie переносит непрозрачный ID, но не принимает решение о доступе. Сервер хранит запись сессии, её статус, срок и связь с текущей версией. Rotation заменяет current ID и делает старый ID непригодным. Logout отзывает серверную запись и отправляет браузеру cookie с тем же scope в прошлом. Эти действия связаны, но не заменяют друг друга.</p>\\\n<h2>Разделите четыре разных факта</h2>\\\n<p>Аутентификация отвечает на вопрос «кто прошёл вход». Эта статья не моделирует сам вход. После него приложение создаёт серверную сессию и связывает её с identity. Дальше каждый защищённый запрос проходит несколько границ. Если их склеить в одну проверку <code>if (cookie)</code>, система начнёт путать носитель, состояние и право.</p>\\\n<p>Первый факт — доставка cookie. User agent прикладывает значение, если имя, host, Path, Secure и SameSite подходят запросу. Это только входная строка. Она может быть старой, отозванной или украденной. Даже отсутствие cookie не доказывает, что серверная запись исчезла.</p>\\\n<p>Второй факт — серверная запись. Она хранит ID или его безопасный отпечаток, identity, статус <code>active</code>, <code>rotated</code> или <code>revoked</code>, срок действия и поколение. Обработчик сначала находит запись и проверяет её. Только после этого он передаёт подтверждённый контекст в authorization.</p>\\\n<p>Третий факт — lineage. Это связь последовательных версий одной сессии. Она отвечает на вопрос «какой ID сейчас current». После rotation у линии должен остаться один current ID. Старый ID можно сохранить для диагностики, но нельзя снова сделать его действующим.</p>\\\n<p>Четвёртый факт — право на операцию. Active session не означает право менять профиль, выплачивать деньги или читать административные данные. Authorization отдельно проверяет subject, ресурс и действие. Наличие cookie не даёт ни одной из этих гарантий.</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>Session record</td><td>Принимать ли ID?</td><td>status, expiry и current pointer</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 flags не являются авторизацией</h2>\\\n<p><code>Secure</code> ограничивает отправку cookie защищённым каналом. Он не шифрует запись в базе и не отзывает её при logout. <code>HttpOnly</code> убирает значение из обычного JavaScript API. Он уменьшает поверхность кражи через клиентский код, но не устраняет XSS и не запрещает серверу ошибочно принимать старый ID.</p>\\\n<p><code>SameSite=Lax</code> ограничивает часть cross-site отправок. Это слой защиты браузера, а не проверка mutation endpoint. Потоки с несколькими доменами, embed или внешним провайдером могут потребовать другую политику. Нельзя объявлять запрос безопасным только по одному flag.</p>\\\n<p><code>Max-Age</code> и <code>Expires</code> задают срок хранения в user agent. Браузер может удалить cookie раньше. Серверный timeout живёт в другом месте и должен проверяться независимо. Поэтому «cookie ещё пришла» не означает «сессия ещё активна», а «cookie исчезла» не означает «logout дошёл до сервера».</p>\\\n<p>Префикс <code>__Host-</code> подходит для host-only cookie: нужны <code>Secure</code>, <code>Path=/</code> и отсутствие <code>Domain</code>. Это фиксирует область доставки. Префикс не выдаёт identity, не проверяет права и не закрывает доступ после отзыва записи. Если cookie должна работать на нескольких поддоменах, такой scope не подходит.</p>\\\n<figure><img src=\"/assets/editorial/2023/sessions-auth-2023-lifecycle.svg\" alt=\"Жизненный цикл одной учебной сессии: active, rotation, stale ID и logout\" loading=\"lazy\" /><figcaption>Учебная схема разделяет серверный переход active → rotated → revoked и доставку нового или очищенного cookie. Это не trace браузера и не доказательство поведения конкретного приложения.</figcaption></figure>\\\n<h2>Rotation должен менять current ID атомарно</h2>\\\n<p>Rotation нужен, когда сервис хочет заменить предъявляемый идентификатор: после входа, повышения доверия или другого заданного события. Сначала сервер проверяет, что пришёл current ID. Затем одна операция помечает predecessor как <code>rotated</code>, создаёт successor с новым поколением и передвигает pointer. Ответ выдаёт cookie successor.</p>\\\n<p>Поздний запрос со старым ID должен получить отказ вроде <code>stale-session</code>. Он не должен продлевать старую запись, повторно создавать successor или менять данные. Нельзя решать эту задачу одним TTL. TTL отвечает за срок, а rotation — за замену владельца current ID.</p>\\\n<p>В реальном хранилище нужна линейзация: транзакция, conditional update, compare-and-swap или эквивалентная гарантия. Два параллельных запроса не должны выпустить два current successor. Учебный пример ниже фиксирует требование к переходам, но не моделирует конкурентность, браузер, сеть или базу.</p>\\\n<pre><code>const current = sessions.findById(request.cookies[SESSION_NAME]);\\\n\\\nif (!current || current.status !== 'active' || current.expiresAt &lt;= now) {\\\n return response.status(401).end();\\\n}\\\n\\\nconst successor = rotateOnce(current); // transaction or compare-and-swap\\\nsetCookie(response, SESSION_NAME, successor.id, {\\\n secure: true,\\\n httpOnly: true,\\\n sameSite: 'lax',\\\n path: '/',\\\n});\\\n\\\n// A later request with current.id must return stale-session.\\\nreturn response.json({ status: 'rotated' });</code></pre>\\\n<p>Этот код — учебная форма контракта. В нём нет настоящих секретов, обработки ошибок хранилища и выбора политики reauthentication. Функция <code>rotateOnce</code> должна сделать проверку и переход одной защищённой операцией. Если она только читает запись, а потом отдельно пишет новую, пример не решает гонку.</p>\\\n<h2>Logout состоит из двух действий</h2>\\\n<p>Серверная ветвь закрывает доступ. Она находит предъявленный current ID, помечает запись <code>revoked</code> и убирает его из current pointer. Повторный запрос с тем же ID получает отказ. Если запрос уже stale, он не должен отозвать successor: иначе поздний ответ в старой вкладке выключит новую сессию.</p>\\\n<p>Клиентская ветвь убирает удобный носитель. Ответ возвращает пустое значение с теми же именем, host, Path и, если он был, Domain. Дата истечения должна быть в прошлом. Совпадение scope важно: clear cookie с другим Path может оставить исходное значение.</p>\\\n<p>Эти ветви доказывают разное. Clear cookie улучшает состояние браузера и интерфейс. Только server-side reject доказывает, что отозванный ID больше не принимают. Если logout защищён от CSRF, это отдельная проверка. SameSite не заменяет её во всех потоках.</p>\\\n<h2>Симптом → причина → проверка → действие</h2>\\\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>Старый ID проходит после rotation</td><td>Handler проверяет наличие cookie, а не status и current</td><td>Отправить predecessor после успешного rotation</td><td>Отклонять rotated ID до protected effect</td></tr><tr><td>Logout меняет только интерфейс</td><td>Удалили cookie, но не отозвали запись</td><td>Повторить запрос с сохранённым ID</td><td>Revoke-ить запись и проверить ответ 401/403</td></tr><tr><td>Поздний logout закрывает новую сессию</td><td>Операция не различает stale и current</td><td>Сначала сделать rotation, затем logout старым ID</td><td>Выбрать scope logout-current, lineage или all и закрепить его</td></tr><tr><td>Cookie живёт не там, где её чистят</td><td>При issue и clear различаются Path, Domain или host</td><td>Сравнить оба Set-Cookie по каждому атрибуту</td><td>Сформировать clear из того же scope-контракта</td></tr><tr><td>Два запроса создают два successor</td><td>Rotation разделён между чтением и записью</td><td>Проверить concurrent path в разрешённой среде</td><td>Добавить транзакционную или conditional линейзацию</td></tr></tbody></table>\\\n<h2>Порядок проверки</h2>\\\n<ol><li>Назовите один поток: issue, protected request, rotation или logout. Зафиксируйте identity, session ID, status, expiry и current pointer.</li><li>Снимите наблюдаемое поведение до исправления. Не называйте проблему инцидентом без запроса, ответа и безопасного идентификатора корреляции.</li><li>Проверьте cookie delivery отдельно: имя, Secure, HttpOnly, SameSite, Path, Domain и срок хранения. Запишите, какой вопрос каждый flag не решает.</li><li>Проверьте server record до действия: неизвестный, expired, rotated и revoked ID должны идти по отказному пути.</li><li>Проверьте rotation на predecessor и successor. После перехода должен существовать один current ID, а старый не должен продлеваться.</li><li>Проверьте logout текущим и stale ID. Current закрывает выбранный scope. Stale не выключает successor, если политика этого не требует.</li><li>Проверьте authorization после session validation. Active session должна давать только явно разрешённые действия.</li><li>Проверьте гонку и ошибку хранилища в разрешённой интеграционной среде. Учебный пример не заменяет этот сценарий.</li></ol>\\\n<h2>Ограничения и критерий готовности</h2>\\\n<p>Модель не выбирает SQL, кеш, signed token или framework store. Она не моделирует несколько устройств, вкладки, clock skew, reverse proxy, reauthentication, CSRF-токены и сетевой порядок ответов. Идентификаторы в примере учебные. Их нельзя использовать в production. Для logout всех устройств нужна отдельная операция, которая явно выбирает identity и все её записи.</p>\\\n<p>Работа готова, если можно показать четыре независимых доказательства: запрос с revoked или rotated ID получает отказ; rotation оставляет ровно один current ID; logout очищает cookie с тем же scope; active session без подходящего authorization получает отказ. Для конкурентного rotation дополнительно нужен тест, который не допускает двух successor. Если доказательство есть только в UI или только в памяти, контракт ещё не проверен.</p>\\\n<h2>Проверяемые источники</h2>\\\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc6265.html\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 6265: HTTP State Management Mechanism</a> — описание Cookie и Set-Cookie, области Path/Domain, Secure, HttpOnly и сроков хранения.</li><li><a href=\"https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener noreferrer\">OWASP Session Management Cheat Sheet</a> — официальные рекомендации по идентификаторам, timeout, logout и проверке управления сессией.</li></ul>"
}