Support case system root-cause analysis
HAR files uploaded for troubleshooting carried live session tokens usable to impersonate valid users; 134 customers affected.
When an attacker holds your session cookie, the server cannot tell their request from yours, because possession is the entire proof. This guide reconstructs, from the Okta, Cloudflare, GitHub, Meta and Microsoft records, why the controls teams reach for after a theft leave that fact untouched, and what it actually costs to bind a credential to a holder who cannot be copied.
The problem, stated without the word "token": how do you let a request prove it comes from someone who already logged in, without letting anyone who copies that proof become them.
Every logged-in request carries a small piece of evidence that a login already happened: a cookie, a bearer token, an API key. The server checks that the evidence is well-formed and unexpired, and if it is, the request is treated as the user. This is the bearer model, and it is nearly universal because it is cheap. The evidence travels in one header, it can be checked without a database lookup if it is a signed token, and it survives across the dozens of services a single page touches. The property that makes it cheap is the same property that makes it dangerous: the evidence is portable. Anyone holding a copy presents the same proof as the person it was issued to, and nothing downstream can tell the two apart.
That is not a hypothetical. It is the exact mechanism behind the incident that put this topic on every architect's desk. In October 2023, attackers who had accessed Okta's support case system found HTTP Archive (HAR) files that customers had uploaded for troubleshooting, and those files contained live session cookies. Okta's own root-cause analysis states it plainly: HAR files "can contain sensitive data, including cookies and session tokens, that malicious actors can use to impersonate valid users" [1]. The attackers did not crack a password or defeat multi-factor authentication. They copied a proof of a login that had already cleared both, and replayed it. 1Password described the same thing happening in its tenant: "an unknown actor used the same Okta session from that HAR file to access the Okta administrative portal" [6].
The reflexes after a session-theft incident, add MFA, cut the token lifetime, rotate the credential, do not change the one fact that made the theft work: the server still cannot tell the thief's copy from the owner's, because possession is the whole proof. Those controls shrink the window. Only binding the credential to something the thief cannot copy, a device key, a proof-of-possession key, a network context, changes the answer to "does this work for someone who is not you". And even binding, as the DBSC authors say themselves, has a documented ceiling.
What this guide covers: the session and access credentials that authenticate a request after login, for both human browser sessions and machine-to-machine calls, and the three families of response to theft: shrink the window, bind the credential, and cut the detection-to-revocation time. What it deliberately does not cover: the login itself (password strength, MFA factor choice, phishing-resistant enrolment), authorization logic once identity is established (covered in the sibling guide on deciding who may do what), and secret management for issuing credentials (see when the secret leaks).
The reference shape shared by every serious answer to session theft: issue the credential bound to a holder, check the binding on use, and keep a channel that can revoke it before it expires.
The systems that take this seriously all converge on the same three places to intervene, even though they use different words and different cryptography. The clearest single statement of the problem comes not from a vendor blog but from a Kubernetes design document, KEP-1205, which set out to fix exactly this for machine identity. It names two of the three bindings directly: "JWTs are not audience bound. Any recipient of a JWT can masquerade as the presenter to anyone else," and "JWTs are not time bound. A JWT compromised via 1 or 2, is valid for as long as the service account exists" [12]. Audience, time, and, the third that KEP-1205 calls object binding, are the levers. Every design below is some combination of them.
The credential is minted with a link to something the legitimate holder has and a thief does not: a private key the client proves possession of (DPoP, mTLS), a device key sealed in a TPM (DBSC), or an audience so a token for service A is useless at service B (KEP-1205). The link is the whole point; a bearer token has no such link.
Bound this way at: Kubernetes, Chrome DBSC, DPoP (RFC 9449)
Each request re-proves the link. DPoP attaches a fresh signed proof per request; DBSC holds the request while it refreshes a short-lived cookie against a key challenge; Okta's post-breach control re-checks the network context and forces re-auth "if we detect a network change" [2]. The resource server's question changes from "is this token valid" to "is this token valid and presented by its holder".
Enforced this way at: Okta, Microsoft Entra
Binding buys detection and a short residual window, not immunity, so a channel is
needed to kill a session on a critical event before its lifetime ends. CAEP
standardises this as a push event, session-revoked, sent from the identity
provider to cooperating resources [16]. Without it, a self-validating token cannot be
recalled; with it, revocation becomes an event rather than a wait.
Built this way at: OpenID Shared Signals (CAEP), Slack
The divergence points are worth naming because they are where an architect actually chooses. Browser sessions bind to a device key because a browser cannot be trusted to hold a per-site secret safely against local malware, so Chrome seals the key in a TPM and refreshes cookies behind it. API and service clients bind with proof-of-possession because they can hold a private key directly, which is what DPoP and mTLS assume. Machine workloads inside a cluster bind by audience and time, because KEP-1205's threat is a token leaking to the wrong in-cluster component, not a stolen laptop. The reference architecture is one shape; the cryptography under each box is chosen by what the holder can be trusted to keep.
Four forks, each with the option that won in the published record, the one that lost, and the condition under which the loser is the right call.
| Decision | Chosen | Rejected | Because | Evidence |
|---|---|---|---|---|
| Token binding | Sender-constrained | Plain bearer | Stolen tokens replay directly | RFC 9700, 2025 |
| Revocability | Server session or short JWT + channel | Long-lived stateless JWT | No in-token way to recall it | jsonwebtoken #113 |
| Binding type | Device / PoP / context by holder | One binding for all | Network binding bypassed via API | BeyondTrust, 2023 |
| Response strategy | Prevent + shrink + detect | Any one alone | Each control has a ceiling | Cloudflare, 2024 |
Seven public incidents, grouped by the class of failure. The design rule at the bottom of each card is the part to carry into a review.
Classes A through C are the same bug wearing different clothes: a portable proof reached a holder who was not the user, whether by theft, by pivot, or by a routing race, and the server had no way to notice. Class D is the honest limit of the answer: even after you bind and add a channel, there is a residual window and a coverage map. Naming that window, per credential and per resource, is the number the reader should leave with.
A note on evidence: the containment successes (BeyondTrust's 30 minutes, GitHub's population invalidation) are as instructive as the losses. They are what a fast revocation channel plus a short window actually buys.
Blast radius, residual windows, binding coverage and cost, each with its source and date. Vendor claims are labelled as claims.
| Metric | Value | At | Context | As of | Source |
|---|---|---|---|---|---|
| Re-logins from one token bug | ~90M | Meta | Measured blast radius of a bearer-token leak, incl. precautionary resets | 2018 | [8] |
| Sessions misrouted by a backend race | <0.001% | GitHub | Measured; still required population invalidation | 2021 | [7] |
| Residual validity after revoke (non-CAE) | up to 90 min | Microsoft Entra | Reported; access token keeps working post-event | 2026 | [24] |
| CAE access-token lifetime | up to 28 h | Microsoft Entra | Vendor: lifetime traded up for a revocation channel | 2026 | [26] |
| Detection-to-session-kill | days/hrs → minutes | Slack | Reported outcome of automated response (AER) | 2025 | [22] |
| Windows devices able to back a key | ~60% | Chrome / Google | Measured TPM availability, growing | 2025 | [13] |
| TPM signing latency on request path | P50 200ms / P95 600ms | Chrome / Google | Measured; error rate ~0.001% | 2025 | [14] |
| Frameworks with session-integrity flaws | 9 of 13 | USENIX study | Measured across top web frameworks | 2023 | [19] |
| Recaptured identity records (yearly) | 43.7B → 53.3B | SpyCloud | Vendor claim; scale of the stolen-credential economy | 2025 | [28] |
| Unrotated creds behind Cloudflare breach | 1 token + 3 accts | Cloudflare | Measured; response rotated 5,000 | 2023 | [4] |
The 28-hour CAE lifetime and the SpyCloud identity-record count are vendor figures; treat the first as a design choice Microsoft made (longer lifetime in exchange for a channel) and the second as a directional claim, not an independent measurement. The 90-minute gap is the number to internalise: it is the residual window on a resource that has a revocation channel. A resource without one has the full token lifetime as its window, which is why "shorten the lifetime" is still load-bearing even after you add CAEP. The TPM latency figures matter because binding puts a signing operation on the refresh path; budget for the P95, not the P50.
Every source behind this page, graded and filterable. The full ledger with
one row per claim and the copied supporting quote ships beside this file as
sources.md.
This session's network egress proxy resolved raw.githubusercontent.com and
answered github.com with 403 for every path, so the specs and design docs
(KEP-1205, the DBSC explainer, the CAEP spec, the OAuth security BCP) were read directly
from source. Every other host below (Okta, Cloudflare, BeyondTrust, 1Password, GitHub's
blog, Meta, CISA, USENIX, IETF, Microsoft, Slack) was refused by the proxy at fetch time;
their text and quotes were retrieved through this session's web-search retrieval, which
returns page content, not from memory. The link checker reports those hosts as
unreachable rather than broken. That is an evidence limit worth stating: the quotes are
real and dated, but a reader with open network access should re-verify the ones on blocked
hosts against the primary page.
HAR files uploaded for troubleshooting carried live session tokens usable to impersonate valid users; 134 customers affected.
Okta's durable fix binds admin sessions to network location and forces re-auth on a network change.
The stolen support token was used against Cloudflare; the first lesson drawn is that the session token itself is the crown jewel.
One service token and three service accounts, unrotated because believed unused, were replayed weeks later; response rotated 5,000 credentials.
A console policy blocked the stolen session, so the attacker pivoted to admin API actions the same cookie authorised; detected within 30 minutes.
An actor used the session from an uploaded HAR file to reach the Okta admin portal minutes after upload.
A backend race could misroute a valid session cookie to another authenticated user's browser; all prior sessions were invalidated.
A feature bug emitted other accounts' access tokens; ~50M reset plus ~40M precautionary, ~90M re-logins.
Storm-0558 forged Exchange Online tokens with a 2016 signing key that should have been revoked; Microsoft still cannot say how it was stolen.
A decade-long request for a token-destroy method; there is none, because a self-validating token has no revocation channel.
Recurring "how do I log out" thread with the same answer: rotate the secret (revokes everyone) or keep a per-token record.
Names the bearer problem for machine identity and its three bindings: audience, time, object. The clearest statement in the corpus.
Binds a browser session to a non-exportable device key so a copied cookie fails off-device; states its own non-goals and ceiling.
Standardises the push channel a resource needs to learn a session was revoked before token expiry, including the session-revoked event.
The standard's answer to token theft: sender-constrain access tokens, and rotate refresh tokens with replay detection or sender-constrain them.
Application-layer sender-constraint: the client proves possession of a key per request, so a copied token without the key is inert.
Cross-browser and framework study finding session-integrity flaws in 9 of 13 top frameworks; contributed to 12 CVEs.
The strategic answer: stop trusting network position, re-decide every request from device and user state.
The engineers building the revocation standard frame it as closing the gap between "access changed" and "the token stops working".
Automated detection terminates sessions on high-confidence anomalies, cutting response from days/hours to minutes; a stale cookie is a signal.
A stolen post-login token can be worth more than a password, because it already represents completion of the full login including MFA.
Even with CAE, a locally validated access token stays usable up to 90 minutes after a critical event, and only first-party apps are CAE-capable.
CAE's coverage is uneven and licence-gated: the critical-event half is broad, the policy and risk halves need P1 and P2.
Documents the trade: token lifetime up to 28 hours in exchange for a revocation channel keyed on critical events.
DBSC shipped to Chrome 146 on Windows, keys TPM-backed; the binding approach reached a consumer browser at scale.
Recaptured identity records grew from 43.7B to 53.3B in a year (vendor figure), sizing the stolen-credential economy.
Seven rungs from a revocable session to a bound one with a measured residual window. The line from toy to real is crossed at rung 4.
Issue a random session id, keep session state server-side, and make logout delete the record. Do not start with a stateless JWT.
Done when: logging out in one tab makes the other tab's next request 401. Teaches: revocability is a property of where the truth lives, not of the token format.
Give access tokens a short life and rotate refresh tokens on every use; if an old refresh token reappears, revoke the whole chain, as RFC 9700 requires.
Done when: replaying a used refresh token kills the session instead of minting a new one. Teaches: rotation turns a stolen refresh token into a detectable event.
Add a per-user revocation record and a way to invalidate all of a user's sessions at once, the control GitHub and Meta both had to exercise at population scale.
Done when: one admin action invalidates every session for a user across devices. Teaches: the blast radius of a bearer leak is the population, so revocation must be too.
Cross into production shape: bind the token to a client key with DPoP, so a request must carry a fresh signed proof and a copied token alone is inert.
Done when: replaying a captured token from a different client (no key) is rejected. Teaches: binding changes the server's question from "valid?" to "valid and yours?".
Adopt a DBSC-style flow: a device key seals the session, cookies are short-lived and refreshed behind a key challenge. Handle the device that has no TPM.
Done when: exporting the cookie to another machine and replaying it fails. Teaches: the ~40% without a key is a design case, not an edge case.
Wire a CAEP-style push so a critical event (disable, password reset, risk) reaches resource servers, not just the issuer, and measure which resources subscribe.
Done when: disabling a user enforces at a resource before the token expires. Teaches: a channel only helps subscribers; the rest have the full lifetime as their window.
Emit a metric for time-from-revoke-to-enforced per resource, and run the attack yourself: steal your own cookie, prove it is inert off-device and dies fast on revoke.
Done when: you can quote the residual window as a number per resource, like Entra's 90 minutes. Teaches: the ceiling is a measurement, not a feeling.
The queries that surfaced this material. The page goes stale in a year; the method does not.
okta HAR file session token root cause 2023cloudflare thanksgiving 2023 incident "failed to rotate" service tokengithub "authenticated sessions" race condition invalidated all sessionsfacebook "view as" access tokens 90 million reset security updateCSRB Storm-0558 forged tokens signing key "should have been revoked"KEP-1205 bound service account tokens "not time bound"device bound session credentials explainer TPM non-goalsCAEP session-revoked openid shared signals specRFC 9700 refresh token rotation reuse detection sender-constrainedcontinuous access evaluation "up to 90 minutes" gap first-partynode-jsonwebtoken issue destroy token invalidate logoutDPoP RFC 9449 adoption production who supports"cookie crumbles" usenix session integrity frameworkstoken binding chrome "intent to remove" why low adoptionslack anomaly event response automatic session termination minutesbeyondcorp usenix login access "regardless of network location"infostealer stolen session cookies statistics annual reportokta session token binding network location admin console