8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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 <= 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>"
|
||
}
|