JWT Missing aud Claim Vulnerability
Skipping JWT aud validation enables the confused deputy attack: a token issued for one service is replayed against another sharing the same signing key. Learn the attack and the one-line fix.
Skipping `aud` (audience) validation is the most commonly skipped: and most dangerous: claim check. A valid JWT signature proves the token was signed by the right key; it does not prove the token was issued for your service. Without `aud` validation, a token issued for one of your services can be replayed against any other service that trusts the same signing key. This is the confused deputy attack.
How the attack works
An authorization server issues tokens for multiple services, all sharing the same signing key. The attacker signs up on a low-security service (analytics) and receives a valid JWT with `aud: analytics.example.com` and a low role. The attacker presents this token to a high-security service (payments) that shares the same signing key. payments verifies the signature: valid. No `aud` check means payments accepted a token never intended for it. The attacker is now authenticated at a service they never signed up for, with the role their analytics token granted.
The fix
Pass the expected audience explicitly on every verification call. Most JWT libraries do not enforce `aud` unless you opt in: do not assume your library validates it by default. Test it: issue a token with the wrong audience, verify without specifying an expected audience, and confirm the library rejects it. If it accepts the token, configure the check explicitly.
Code examples
The attack (confused deputy)
Setup:
auth.example.com issues tokens for multiple services.
analytics.example.com: easy signup, low-security.
payments.example.com: requires elevated role, high-value target.
Both services trust the same signing key.
Attack:
1. Attacker signs up on analytics.example.com.
2. Attacker receives: { sub: "attacker", aud: "analytics.example.com", role: "analyst" }
3. Attacker presents this token to payments.example.com.
4. payments verifies the signature: valid.
5. No aud check → payments accepted a token never intended for it.
6. Attacker is authenticated at a service they never signed up for. The fix (Node.js: jsonwebtoken)
jwt.verify(token, secret, {
algorithms: ["HS256"],
issuer: "https://auth.example.com",
audience: "https://payments.example.com", // reject tokens for other services
}); Frequently asked questions
-
What is the JWT confused deputy attack?
The confused deputy attack exploits missing aud (audience) validation. An attacker obtains a valid JWT issued for a low-security service they can sign up for (e.g. analytics), then replays it against a high-security service that shares the same signing key (e.g. payments). The signature verifies, but without an aud check the high-security service accepts a token that was never intended for it. The fix is to pass the expected audience to every verification call so the library rejects tokens issued for other services.
-
Why is my JWT aud rejected with 'Audiences in jwt are not allowed'?
This Spring Security error means the token's aud does not contain the audience Spring's decoder is configured to expect. The fix is to align the token's aud claim with the audience configured in application.yml (spring.security.oauth2.resourceserver.jwt.audiences) or in your custom OAuth2TokenValidator. Do not disable the audience validator to make the error go away: that opens the confused deputy attack. See the /verify/java/ guide for the full troubleshooting walkthrough.
-
Does my JWT library validate aud by default?
Often no. Many JWT libraries make aud validation opt-in: you must pass the expected audience explicitly to the verify call. Do not assume your library validates it. Test it: issue a token with the wrong audience, verify it without specifying an expected audience, and confirm the library rejects it. If it accepts the token, configure the audience check explicitly. jsonwebtoken, PyJWT, jjwt, and Spring Security all require you to pass the expected audience.