Who Signs Your Token: Reading jku, kid, iss and aud Correctly
The challenge
Caldera's shipment API accepts signed tokens from partner carriers. A token arrives, the API needs the public key that will verify it, and the library it uses is helpful: rather than making the team maintain a list of partner keys, it reads where to fetch the key from the token itself. That is a real header field, it is in the specification, and libraries really do honour it. Three different hostnames appear in this token and they are all plausible. Only one of them decides whether the signature is trusted, and it is the one an attacker gets to choose. Decode the token and submit the host the verifier would go to for the key.
What you'll learn
- Tell header parameters apart from payload claims and know which are trusted
- Explain what jku means and when a library will act on it
- Explain why iss cannot be used to choose a verification key
- Describe how an attacker forges a valid signature by supplying the key
- Spot a lookalike hostname standing in for partner infrastructure
Skills tested
Prerequisites
- The three segments of a JWT
- That base64url is encoding, not encryption
How it works
Verifying a signature is two questions, not one. Is the signature mathematically valid, and is it valid under a key you actually trust? Most JWT bugs live in the second question, and this one lives there entirely.
The token names three hosts. iss is https://auth.brightway.example, an identity claim in the payload. aud is https://api.caldera.example, the intended recipient. Neither is a key source, and iss in particular cannot be one: it sits inside the body whose signature you have not checked yet, so using it to choose the key would be circular.
The third is in the header. jku stands for JWK Set URL and it is defined in RFC 7515. It means: the keys that verify this token are published here. A library that honours jku will make that request before it validates anything, which puts jwks.brightway-cdn.example in charge of whether the signature is trusted.
Nothing protects the header. The sender writes it. So an attacker generates a key pair, signs a token carrying any claims they want, publishes the matching public key as a JWK Set on a host they control, sets jku to it, and the verifier downloads that key and confirms the signature. The mathematics is correct throughout. The key was the attacker's.
kid is the field people expect this bug in, and it is not the bug here: prod-2026-08 identifies a key without saying where it lives. The lookalike hostname is the finishing touch. Next to auth.brightway.example, a host called brightway-cdn.example reads like the same company's CDN, and it is a completely separate registration.
Common mistakes
- Answering auth.brightway.example.
isssays who claims to have issued the token, and it is inside the part you have not verified. - Answering api.caldera.example.
audis the recipient, checked after verification, never a key source. - Answering prod-2026-08.
kidnames a key; it is not a host and not a URL. - Assuming the header is protected because the token is signed. The signature is only meaningful once you have a trusted key, and the header is what chooses it.
- Reading brightway-cdn.example as brightway.example. They are different registrations, which is the entire point of the name.
How to defend against it
Never let a token tell you which key verifies it.
- Disable
jkuandx5uin the verifier. Most libraries have a flag or an allowlist of header parameters; if yours resolves them by default, that is the setting to change today. - Pin key discovery to configuration. Map each issuer to a JWKS URL you control, and fetch only from that map.
- Use
kidonly to select among keys you already hold, never to fetch a new one, and fail closed on an unknownkid. - Pin the expected algorithm per issuer and reject anything else, so a token cannot switch you to
noneor to HMAC with a public key as the secret. - Validate
issandaudafter the signature, not before, and alert on any token whose header carries a URL parameter at all. In a healthy deployment that number is zero.