6 Minute Read
Modern cloud ecosystems often place a single identity provider in charge of handling logins and tokens for a wide range of customers. This approach certainly streamlines single sign-on (SSO) for end users, but it also places enormous trust in a single set of signing keys. If those private keys are compromised, attackers can create tokens that appear valid to any service that relies on them. Storm-0558 is a prime example of how this can backfire, because a key that was intended for one context ended up creating tokens accepted in a different environment, bypassing every normal safeguard. In the Storm-0558 incident, a signing key intended for Microsoft consumer accounts ended up in the wrong hands. That same key was then used to issue tokens for enterprise Azure AD services. Azure AD did not differentiate consumer keys from enterprise keys, so the forged tokens were given access to resources such as Outlook Web Access. Rather than being contained, a single stolen key became a universal pass across multiple tenants, which is a worst-case outcome for multi-tenant services. Typically, tokens such as OAuth or OpenID Connect rely on cryptographic signatures to confirm the issuer. A resource server checks the signature against the public key from the identity provider, and if it matches, the token is trusted. However, if the provider fails to retire or strictly isolate keys, one breached key can be used to sign tokens for any user. That is exactly what occurred with Storm-0558. The attacker adjusted claims (for example, sub or tid) to choose a particular account or tenant, then signed the token with the compromised key. Since Azure AD did not reject that key, the forgeries appeared legitimate, granting full access to email systems, user data, and more. Though this event targeted Microsoft, the underlying issue is not exclusive to Azure AD. Anyone who acquires a private key from an OAuth or OIDC deployment can forge tokens with arbitrary claims, unless the application enforces correct checks on the issuer, audience, and tenant. If those checks are missing, a token meant for tenant A might be accepted by tenant B. Similar trouble can arise in open-source solutions, including Keycloak, where each realm has its own key. Once that key is stolen, an attacker can impersonate any user in that realm. Some multi-tenant environments even share a single JSON Web Key Set (JWKS) or key set across many tenants, making one compromised key an easy entry point for multiple realms. Without logs tying each token to a particular tenant or issuer, these attacks can go undetected. Attackers use a variety of methods to obtain or misuse keys. They might capture them from memory dumps or guess them if they are weak Hash-based Message Authentication Code (HMAC) secrets. Once they have a valid key, they can sign tokens indefinitely, adjusting claims to escalate privileges. Attackers may also manipulate the token structure by switching alg to none if the system fails to block that, or by changing the kid field to load a key from a file path they control. JKU injection is another trick, pointing the server to a malicious URL to fetch a matching public key. The attacker can also set an unusually long exp value, adjust aud to reuse a token for a different purpose, or add claims that label a token as an admin. Similar incidents have arisen elsewhere. During the SolarWinds hack, attackers stole a SAML signing certificate from an ADFS server, letting them generate valid tokens for Microsoft 365. OAuth audience confusion has caused tokens created for one service to be improperly accepted by another. Researchers have discovered JWT libraries that allowed alg=none or confused RS256 with HS256, letting a public key act like a shared secret. Some identity providers neglect to retire outdated keys or skip checks on iss and aud, letting tokens cross boundaries they never should. The good news is that Storm-0558 and similar breaches highlight actionable steps for defending your environment against forged tokens. Below are concrete methods and example configurations that you can adapt. 1. Strong Key Management and Rotation A single compromised signing key opens the door to forging tokens for any user in your environment. Storing those keys in unsecured locations, such as a file system or code repository, increases the risk of accidental leakage or targeted theft. How to Implement Nginx Example: This config uses AWS KMS built-in rotation and ensures only certain roles can sign with this key. 2. Comprehensive Token Validation A valid signature alone is not enough. Attackers can reuse tokens across different tenants or resources if you fail to check crucial claims like iss, aud, or tid. How to Implement JS Sample: This verifies the audience, issuer, and allowed algorithms, as well as checks for a valid tenant ID. 3. Logging and Monitoring for Suspicious Tokens If you do not track which key or issuer was used to validate a token, forged tokens might go unnoticed. Proper logging lets you spot red flags quickly. How to Implement Example Log Output: Having structured logs like this in JSON makes it easier for your SIEM to parse and act on anomalies. 4. Testing for Common JWT Flaws Attackers routinely exploit known JWT weaknesses such as alg=none or confusion between HS256 and RS256. If your app or library is poorly configured, it may accept tokens that are not genuinely signed. How to Implement Example Python Script: If the response indicates success, you have a problem. 5. Separating Environments, Tenants, and Keys Storm-0558 showed how a key for consumer use was accepted in enterprise contexts. Whenever you use a single-identity provider for multiple tenants, you should ensure each tenant is isolated at the key level. How to Implement Keycloak Configuration Example: This makes sure tokens only come from tenantA’s realm. 6. Handling Incidents and Rapid Response Even the best setups can face breaches. You need a plan if a key leaks or forged tokens are found in the wild. How to Implement Storm-0558 is a cautionary tale of what happens when a single signing key crosses boundaries it was never designed for. Many identity solutions will trust any key they have not explicitly blocked, so if a key from one environment appears in another, it can lead to serious breaches. The best defense is to secure keys in a vault or HSM, enforce strict validation rules for each issuer and tenant, log all token data to spot anomalies, and test for well-known JWT pitfalls such as alg=none or HS256 vs RS256 confusion. Ensure that each realm or tenant uses its own key sets and have a plan to revoke keys quickly if they are compromised. By following these steps, you greatly reduce the odds that one stolen key can bring down your entire identity framework.How Storm-0558 Happened
Practical Steps to Protect Against Token Forgery
# Example: AWS KMS Key Rotation in Terraform
resource "aws_kms_key" "app_signing_key" {
description = "KMS key for signing application tokens"
enable_key_rotation = true
# ...
}
const jwt = require("jsonwebtoken");
function validateToken(token, expectedAudience, allowedIssuers, publicKey) {
const options = {
audience: expectedAudience,
issuer: allowedIssuers,
algorithms: ["RS256", "ES256"]
};
const decoded = jwt.verify(token, publicKey, options);
if (decoded.tid && decoded.tid !== "expected-tenant-id") {
throw new Error("Invalid tenant");
}
return decoded;
}
{
"event": "token_validation",
"timestamp": "2025-01-01T12:00:00Z",
"kid": "abc123",
"iss": "https://login.example.com",
"sub": "user123",
"aud": "my-api",
"tid": "tenantA",
"roles": ["user"]
}
import jwt
import requests
def test_alg_none(url):
headers = {"alg": "none", "typ": "JWT"}
payload = {"sub": "admin", "iat": 1690846103}
token = jwt.encode(payload, None, algorithm=None, headers=headers)
resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
print("alg=none test:", resp.status_code, resp.text)
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: "https://id.example.com/realms/tenantA"
Conclusions