Ошибка в сессиях часто выглядит спором о flag: добавить SameSite, HttpOnly или Max-Age. Проблема глубже, если непонятно, где решается валидность ID после rotation/logout. Тогда один handler верит cookie, другой TTL в базе, третий очищает только клиент. Цена — противоречивый доступ или потеря новой сессии поздней операцией со старым ID.
Достаточно выбрать один поток и владельца каждого факта. Здесь cookie переносит непрозрачный session ID; server record хранит status и generation; lineage указывает на один active ID. Rotation меняет record и выдаёт cookie, logout revoke-ит record и возвращает clear cookie. Fixture проверяет это в памяти, без браузера, storage/network trace и вывода о реальной платформе.
Пять состояний, которые не стоит склеивать
Фраза «сессия истекла» скрывает разные факты: cookie нет, но record ещё жив; cookie есть, но record revoked; ID известен, но уже не current; доступ отклонён по правам при active сессии. Поздний Set-Cookie тоже не доказывает порядок его применения браузером. Эти состояния нельзя склеивать без риска случайно изменить authorization.
Модель не выбирает SQL, cache, signed token или framework store. Она требует лишь отличить current от rotated/revoked до защищённого эффекта. Порядок Cookie и похожее имя не должны решать доступ: RFC 6265 не советует полагаться на сериализацию одинаковых cookie.
| Слой | Вопрос | Данные в учебной модели | Нормальный переход | Неверный вывод |
|---|---|---|---|---|
| Аутентификация | кто прошёл вход? | в fixture не представлена | создаёт initial session record | cookie сама доказывает личность |
| Cookie delivery | какую строку браузер приложит? | __Host-session и flags | issue, rotate, clear | Max-Age решает серверный timeout |
| Server record | принимать ли этот ID сейчас? | status и generation | active → rotated или revoked | наличие ID равно доступу |
| Lineage | какой ID является current? | fixture-lineage-a → fixture-s-2 | pointer сдвигается при rotation | старый ID можно повторно продлить |
| Authorization | можно ли выполнить действие? | в fixture не представлена | проверяется отдельно после ID | active session даёт любое право |
Flags: точные границы вместо списка галочек
Secure ограничивает канал доставки с точки зрения user agent, но не подписывает server record. HttpOnly закрывает значение от не-HTTP API, но не лечит XSS и не отменяет запросы браузера с cookie. Оба flag описывают рядом с их узким эффектом.
SameSite=Lax ограничивает часть cross-site доставок. Draft-11 различает Strict, Lax и None; Lax допускает safe top-level navigation помимо same-site запросов. Это защита в глубину, не доказательство корректности mutation endpoint. Embed или federation могут потребовать другой контекст и отдельную проверку server request.
Max-Age/Expires задают максимум browser хранения, но user agent может удалить cookie раньше. В draft-11 Max-Age приоритетнее Expires; сервер не обязан принимать ID до того же момента. NIST SP 800-63B не предлагает полагаться на expiry cookie для timeout: browser lifetime и server expiry хранят отдельно.
Почему __Host- полезен только при подходящем scope
Имя __Host-session означает не расширять cookie на поддомены. Для conformant user agent префикс требует Secure, Path=/ и отсутствия Domain, поэтому issue и clear имеют один scope. Это не namespace безопасности и не замена ревью hostnames. Если cookie нужна на нескольких поддоменах, __Host- не подходит: Domain и его владельцев надо описать явно.
Path=/ даёт cookie доступ на всём host и выполняет условие префикса, но не делает путь security boundary. Разделение административной области обеспечивает server authz и архитектура hostnames.
Rotation: сохранить lineage, заменить предъявляемый ID
Rotation меняет значение, которое сервер считает current. Fixture оставляет fixture-s-1 rotated, добавляет fixture-s-2 generation=2 и переписывает pointer. Старый ID получает stale-session, а не новый TTL: сервер не смешивает известный, но заменённый ID с действующим.
Реализация должна определить линейзацию: транзакцию, conditional update, compare-and-swap или другую гарантию хранилища. Иначе два запроса выпустят два successor. Fixture не моделирует параллелизм, но его staleRotationDoesNotCreateAnotherSession — требование к реальному механизму.
Logout: явный scope важнее красивого 204
Scope logout называют до кода. Logout-current revoke-ит один record; logout-all ищет все records identity; logout-lineage лежит между ними. Их нельзя скрыть за единым endpoint без явного правила: stale запрос иначе либо оставит доступ, либо выключит новую сессию.
Fixture выбирает logout-current: fixture-s-1 после rotation получает stale-session и не трогает fixture-s-2. Это инвариант выбранного contract, не правило для любого сервиса. Если нужен другой эффект, создают logout-lineage/logout-all и проверяют его последствия отдельно.
Проверяемый код и ожидаемые границы
Пример воспроизводим, потому что не зависит от времени, сети или secret store. В нём нет пользователя, браузерных API и настоящего Set-Cookie parser. Он проверяет только переходы: rotation меняет pointer, stale операции не изменяют successor, current logout revoke-ит record и формирует clear cookie.
import {
createTeachingSessionState,
rotateTeachingSession,
logoutTeachingSession,
} from './upgrade-2023-02.mjs';
const before = createTeachingSessionState();
const rotation = rotateTeachingSession(before, 'fixture-s-1');
const oldIdLogout = logoutTeachingSession(rotation.state, 'fixture-s-1');
const newIdLogout = logoutTeachingSession(rotation.state, 'fixture-s-2');
if (oldIdLogout.accepted) throw new Error('stale logout changed successor');
if (newIdLogout.state.records['fixture-s-2'].status !== 'revoked') {
throw new Error('current logout did not revoke record');
}
Для полного набора assertions используется команда node web/scripts/upgrade-2023-02.mjs --verify-fixture. До запуска в проекте прочитайте сами assertions: fixture не умеет обнаружить неверный reverse proxy, заголовки framework, маршрут logout, race в базе или поведение browser cookie jar. Он удобен как короткий договор на review, но не как тест, который заменяет интеграционный сценарий. Производственный тест должен иметь разрешённую среду, безопасные тестовые данные и отдельно названные наблюдения.
Маршрут: симптом → причина → проверка → действие
- Симптом. Найдите место, где код заключает «cookie есть — сессия валидна», или «Max-Age истёк — сервер уже всё закрыл». Это наблюдаемый пробел контракта, а не диагноз всей системы.
- Причина. Разделите browser delivery, server record, lineage и authorization. Для каждого напишите один владелец и один момент изменения.
- Проверка flags. Сверьте name, Secure, HttpOnly, SameSite, Path, Domain и client lifetime с нужным scope. Отдельно запишите, что каждый флаг не решает.
- Проверка rotation. В разрешённом тесте отправьте старый ID после successful replacement и убедитесь, что он не становится current. Для маленького договора сначала выполните fixture.
- Действие. Сделайте серверную проверку status/current обязательной перед защищённым эффектом; определите один явный механизм линейзации rotation и отдельные операции logout-current, logout-lineage или logout-all.
- Откат. Не продлевайте retired ID ради быстрого отката. Подготовьте совместимую ветку или migration plan, которые сохраняют reject старого ID и позволяют объяснить состояние сессии пользователю.
Ограничения модели и следующий шаг
Статья не утверждает, что __Host- или SameSite=Lax подходят каждому приложению. В ней нет оценки AAL, криптографии, устройств, distributed store, CORS/CSRF, browser trace и измерения. fixture-s-1, fixture-t0 и 900 секунд учебные. IETF/NIST задают словарь и ограничения, не принимают за команду решение о scope, UX или гонках.
Следующий шаг — выписать для одного protected handler допустимые status, смысл rotated, владельца lineage и logout scope, затем добавить отдельный интеграционный сценарий с безопасными тестовыми данными. Если client expiry смешан с server revocation, сначала исправляют эту границу.
Историческая граница февраля 2023
Все три источника в конце существовали не позднее февраля 2023: RFC 6265 опубликован в 2011, NIST SP 800-63B — в 2017 с обновлениями 2020 года, а draft-ietf-httpbis-rfc6265bis-11 — 7 ноября 2022. В частности, draft-11 остаётся work in progress и приведён именно с этим статусом. Ссылки нужны для проверки описанной модели, а не как свидетельство запуска или конфигурации неизвестного сервиса.
Проверяемые источники
- IETF RFC 6265: HTTP State Management Mechanism, April 2011 — стандарт IETF, опубликованный в 2011 году. Он задаёт передачу Cookie/Set-Cookie, scope, Secure и HttpOnly; он не задаёт прикладной срок валидности серверной сессии.
- IETF draft-ietf-httpbis-rfc6265bis-11: Cookies: HTTP State Management Mechanism, November 2022 — датированный Internet-Draft, опубликованный 7 ноября 2022 года и доступный к февралю 2023. Это work in progress, а не финальный RFC; здесь он нужен для SameSite и префикса __Host-.
- NIST SP 800-63B: Digital Identity Guidelines — Authentication and Lifecycle Management, June 2017 with updates through 2 March 2020 — официальный датированный документ NIST, доступный к февралю 2023. Его требования относятся к заданному NIST контексту; он помогает отделить cookie от серверного timeout, logout и reauthentication.