Симптом часто звучит так: «в одной вкладке вышли, в другой ещё есть доступ» или «после renewal разлогинило». Из этого не видно, какой ID пришёл на сервер, какой был current и какой logout scope выбран. Цена правки наугад — старая сессия остаётся принятой либо поздний logout закрывает новую.
Ниже — маршрут, не отчёт об инциденте: нет пользователя, вкладок, Network/Storage или HTTP-запуска. Есть fixture одной lineage: fixture-s-1 становится rotated, fixture-s-2 active, старый ID stale, logout нового ID revoke-ит его. Это задаёт вопросы для реального расследования, не выдумывает trace.
Соберите четыре факта до изменения флага
Начинать стоит с формата записи: безопасный surrogate ID, server-side status в момент обработки, lineage/current ID и тип операции. Полный bearer secret в лог не попадает. Нужна корреляция, по которой reviewer сопоставит request с решением, но не повторит сессию.
Отделяйте факт от интерпретации. «Cookie не удалена» требует разрешённой проверки клиента; «logout не дошёл» — evidence запроса или server record; «старый ID прошёл» — статус именно старого ID. Если данных нет, причина не подтверждена: fixture подсказывает инвариант, не историю браузера.
| Симптом | Вероятная причина | Проверка без секрета | Действие |
|---|---|---|---|
| После renewal старый ID ещё выполняет защищённое действие | record не стал rotated или handler не проверяет current | сопоставить безопасный ID записи, status и lineage pointer в разрешённой среде | отклонять rotated ID до эффекта и линейризовать rotation |
| Logout меняет UI, но следующий запрос принят | очищена только cookie или revoke не дошёл до server record | проверить audit event logout и status того же record | revoke server record и вернуть clear cookie одинакового scope |
| Поздний logout отключает новую сессию | не определён scope, stale ID трактуется как current | сравнить предъявленный ID с currentByLineage на момент обработки | выбрать явный logout-current или logout-lineage и добавить invariant |
| После clear cookie видна другая cookie того же имени | issue/clear расходятся в Path или Domain | сравнить атрибуты Set-Cookie без значения bearer ID | удалять cookie теми же name, Path и Domain при наличии |
| Проблема есть только в одном браузере | политика хранения или порядок ответов отличается | повторить разрешённый минимальный сценарий на конкретной версии | не менять server contract до подтверждения различия |
Старый ID — это не обязательно неизвестный ID
Различайте unknown, rotated и revoked. Unknown — store не нашёл ID; rotated — запись известна, но не current и имеет successor; revoked — сессия завершена. Внешне они могут дать один 401, но внутри должны отличаться, иначе нельзя доказать, что rotation вывела старый ID из обращения.
Fixture хранит predecessor ради различия, не как совет хранить records вечно. На период возможных повторов сервер должен отклонять старый ID. RFC 6265 задаёт browser scope доставки; прикладную семантику Cookie определяет сервер.
Воспроизводимый fixture и invariants stale событий
Fixture sidecar создаёт current record, делает accepted rotation, затем повторяет rotate/logout со старым ID. Обе stale операции возвращают accepted=false и оставляют successor active. Logout current ID делает record revoked, удаляет pointer и формирует clear cookie. Есть и положительная, и отрицательная ветви.
import {
createTeachingSessionState,
rotateTeachingSession,
logoutTeachingSession,
} from './upgrade-2023-02.mjs';
const first = createTeachingSessionState();
const replacement = rotateTeachingSession(first, 'fixture-s-1');
const staleRotation = rotateTeachingSession(replacement.state, 'fixture-s-1');
const staleLogout = logoutTeachingSession(replacement.state, 'fixture-s-1');
if (staleRotation.accepted || staleLogout.accepted) {
throw new Error('old ID changed current session');
}
if (replacement.state.records['fixture-s-2'].status !== 'active') {
throw new Error('successor is not active');
}
Команда node web/scripts/upgrade-2023-02.mjs --verify-fixture запускает полный набор assertions скрипта. Он воспроизводим только потому, что значения и порядок вызовов фиксированы. Его PASS не подтверждает, что web server выдал Set-Cookie, что браузер применил ответ, что база сделала compare-and-swap или что logout endpoint защищён от подделки. В review укажите этот предел рядом с результатом. Иначе маленький тест станет ложным evidence для нескольких неиспытанных уровней системы.
Порядок ответа и порядок решения — разные задачи
Даже с правильным server reject клиент может получить ответы в неудобном порядке. Два запроса ушли со старым ID, один принёс новый Set-Cookie, другой ошибку. Browser cookie jar и fetch/navigation имеют свои правила; fixture их не моделирует. Реальный дефект требует отдельного разрешённого repro с версиями, шагами, маскированными значениями и HTTP-метаданными.
Серверный инвариант остаётся: принимается только current active ID; stale событие не создаёт successor и не revoke-ит нового при logout-current. UX после reject выбирает продукт. Не прячьте его бесконечным retry без определения перехода пользователя.
Проверьте clear cookie как договор, а не как строку в шаблоне
Удаление cookie требует того же имени и scope, что issue. В модели clear cookie: __Host-session, Path=/, Secure, HttpOnly, SameSite=Lax и Expires в прошлом. __Host- исключает Domain, поэтому issue/clear не расходятся по поддоменному scope. Если Domain нужен в другом контракте, он обязан быть в документе и проверке очистки; не добавляйте его только на logout.
Flags clear cookie не отменяют server revoke. RFC 6265 связывает замену с name, domain и path и не советует полагаться на порядок одинаковых Cookie. При нескольких Set-Cookie нужен один owner, один scope и проверка отсутствия дубликата в разрешённом сценарии.
Маршрут: симптом → причина → проверка → действие
- Симптом. Запишите наблюдаемое последствие без вывода: какой экран, какой внешний status, какая безопасная корреляция операции. Не называйте браузерный trace, если его не собирали.
- Причина. Проверьте гипотезы по очереди: server record не изменился, handler не сверил current, stale logout имеет неясный scope или issue/clear cookies различаются по атрибутам.
- Проверка модели. Выполните fixture и прочитайте assertions staleRotationDoesNotCreateAnotherSession и staleLogoutDoesNotRevokeSuccessor. Это проверка выбранного правила, не production доказательство.
- Проверка реализации. В разрешённой среде соберите ID surrogate, record status, lineage pointer и Set-Cookie attributes без bearer значения. Сверьте момент обработки, а не только время на клиенте.
- Действие. Сделайте status/current обязательным перед защищённым side effect, выберите понятный logout scope и проверяйте clear cookie на совпадение с issue policy.
- Откат. При проблеме UX не возвращайте старый ID в active. Сначала сохраните reject stale ID, затем примените отдельный совместимый rollback для экрана, маршрута или новой политики cookie.
Ограничения и безопасный результат расследования
Fixture не хранит секреты, не реализует random ID/TLS/часы и не знает browser API, пользователей, устройств, cache, CSRF или конкурентности. Она не доказывает stale logout в продукте и не выбирает HTTP-ответ. Она фиксирует лишь правило logout-current: старый ID не оживляет и не отменяет successor. Другой scope требует другой fixture.
Следующий шаг — добавить audit contract: корреляция без bearer secret, server status до protected effect, logout scope и причина reject. Затем проводить отдельный repro только в разрешённой среде. Несобранный факт остаётся неизвестным; это безопаснее правдоподобной, но неповторяемой истории про вкладки.
Историческая граница февраля 2023
Этот маршрут опирается на документы, которые существовали к февралю 2023: RFC 6265 от 2011 года, NIST SP 800-63B от 2017 года с обновлениями до 2020 и draft-ietf-httpbis-rfc6265bis-11 от ноября 2022. Draft не объявляется финальной спецификацией. Ни один из источников не даёт trace конкретного браузера и не заменяет целевую проверку реального logout flow.
Проверяемые источники
- 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.