In mid-2023 an attacker used a signing key belonging to Microsoft's consumer account service to forge tokens that cloud email accepted for enterprise tenants, and read mail for about 25 organisations. Microsoft's own investigation found a validation flaw as well as a stolen key. What design decisions made one stolen key so damaging, and what is the structural fix?
Show the full answer Hide the answer
The trigger
Microsoft's published investigations (July and September 2023) describe an actor tracked as Storm-0558 acquiring an inactive signing key for the consumer (MSA) identity system and using it to mint tokens. The route by which the key was obtained was never established with certainty; Microsoft named a 2021 crash dump as the likely source, a conclusion later reviewers said the evidence did not confirm. The key mattered less than what it unlocked.
Why it propagated
The relying service accepted tokens signed by a key from the wrong trust domain. Microsoft stated that a flaw in a library used by the mail systems meant that a token signed with a consumer key was accepted as valid for an enterprise identity. Two keysets existed (consumer and enterprise), and the endpoint did not insist that the token's issuer and key matched the tenant type it was serving.
Three amplifiers sit behind that:
- Key scope was broader than its purpose. A key meant to sign consumer tokens could be made to produce a token that was honoured by an enterprise service.
- Long-lived signing material. The key was an old one, long past its intended life, and had not been retired from the validation set.
- A trust root with no secondary check. A single signature on a token was enough to grant mailbox access; there was no independent check that the claimed tenant, issuer and audience agreed.
Why detection lagged
The forged tokens were cryptographically valid, so authentication logs showed legitimate-looking access. It was a customer with detailed mailbox audit logging (the licence tier that included the MailItemsAccessed event) who noticed unusual access and reported it to Microsoft. The detection depended on a paid logging feature, not on the platform's own anomaly detection.
The structural fix versus the tempting local fix
The tempting fix is to rotate the one stolen key, which Microsoft did, along with revoking tokens. It ends the incident and leaves the class open. The structural fix has four parts:
- Bind each key to a trust domain and make validation reject a token whose issuer, key and audience do not match, in shared code with a test for the cross-domain case.
- Short key lifetimes and automatic retirement, with old keys removed from validation sets.
- Keep signing keys in hardware-backed stores and out of environments where crash dumps, debug artefacts or build systems could capture them.
- Make audit logging a default for the actions an attacker would take.
What the fix costs, and the rule for choosing it
Key rotation on a short schedule (for example every 30 to 90 days with an overlap of one period so in-flight tokens stay valid) costs distribution machinery: every validator must fetch the current key set (a JWKS-style endpoint) and cache it with a short lifetime, and an outage of that endpoint becomes an outage of authentication unless validators keep the last good set. Hardware-backed signing adds latency of roughly a millisecond per signature and a throughput ceiling per module, which is fine for token issuance at thousands of tokens a second and is why issuance is usually batched or sharded across modules.
Decision rule: one keyset per trust domain whenever the domains have different blast radii or different owners; a single shared keyset only when one team owns every issuer and validator. The rule flips for a small system with one issuer, where per-domain keysets are overhead and checking issuer and audience is sufficient.
The mechanism to remember is the order of checks. A validator should resolve the key from the token's claimed issuer, then require that the key belongs to that issuer's published set, then check audience and tenant. A validator that first verifies the signature against any key it trusts and only afterwards reads the claims has inverted the order, which is what lets a key from one domain vouch for another.
The general lesson
A signature proves who signed, not that the signer was entitled to say this. Validate issuer, audience and key scope, not just the signature.
When this is more than you need
A single-tenant application with one issuer and one audience does not need domain-bound keysets; validating issuer and audience is enough. The failure appears when one platform runs several trust domains that share validation code.
Common weak answers
- "Rotate keys more often." It limits the window and does not fix cross-domain acceptance.
- "Monitor for forged tokens." A valid signature looks like a real token.