How to validate the JWT aud claim and why it matters
Why skipping audience validation lets attackers replay tokens across services, and how to fix it.
If you work with JWTs, you've seen the aud claim tucked into every token payload alongside iss and exp. It tells you who the token is for. Simple enough, right? But in practice, the aud claim is one of the most commonly ignored claims in production systems, and getting it wrong opens the door to token replay attacks where a legitimate token issued for one service is used to access a completely different one.
This article covers what the aud claim is actually for, the specific ways developers mishandle it, and the concrete validation patterns you should implement to keep your system secure.
!!Need to inspect a token? Use the WorkOS JWT Debugger to decode and inspect your JWTs directly in the browser. It's a quick way to verify your token's aud, iss, sub, and other claims while debugging.!!
What is aud in JWT?
The aud (audience) claim identifies the recipients that the JWT is intended for. As defined in RFC 7519, Section 4.1.3, its value is a case-sensitive string or an array of strings, where each string typically represents a URI or identifier for a service that should accept the token. The aud claim is a recommended claim, and should be included in every JWT.
A typical JWT payload looks like this:
The aud claim here tells the receiving service: "This token was created for https://api.example.com." The receiving service should verify that it recognizes itself as the intended audience before trusting anything else in the token.
The aud claim can also be an array when a token is intended for multiple recipients:
If your service isn't listed in the audience, the token should be rejected.
This sounds straightforward. In practice, it's where a surprising number of token replay vulnerabilities originate.
Why the aud claim matters
Imagine you have two services: a public API and an internal admin API. Both trust the same authorization server (same iss). A user authenticates and gets a token scoped to the public API.
Now imagine a malicious or compromised intermediary gets hold of that token. The signature is valid. The issuer is trusted. The token hasn't expired. So the intermediary forwards it to your admin API, which accepts it, because from its perspective, everything checks out.
This is a token replay attack, and it's the specific threat that aud validation exists to prevent.
Think of it like receiving a sealed letter. The signature on the seal tells you it's authentic (iss). But the address on the envelope tells you whether it was actually meant for you (aud). If you skip audience validation, you're opening and acting on every letter that arrives with a valid seal, even if it's addressed to someone else entirely.
In any system where one authorization server issues tokens for multiple services, every token is valid from a signature perspective across all of those services. A valid signature is the norm across services, not the exception. The only thing that prevents Service A from accepting a token meant for Service B is audience validation.
How to validate the aud claim
When your backend receives a JWT, it should:
- Extract the
audvalue (which may be a string or an array). - Check that your service's identifier is present in the audience.
- Reject the token if your service isn't listed.
This ensures you only accept tokens that were explicitly intended for your service.
Common mistakes with the aud claim
1. Not validating aud at all
The most dangerous mistake is also the most common: simply not checking the audience. Many developers validate the signature, the exp claim, maybe even iss, and assume that's sufficient. It isn't.
Without audience validation, any valid token from a trusted issuer will be accepted by your service, even if it was issued for a completely different service. In environments with multiple APIs sharing the same identity provider, this is a critical gap.
Here's the vulnerable pattern:
And here's what it should look like:
2. Using overly broad audience values
Some developers set the aud claim to something generic like the organization name or a wildcard domain. This defeats the purpose of audience validation entirely. If every service in your organization matches the same audience value, you have no protection against token forwarding between those services.
The audience should uniquely identify the specific API or service the token is intended for. https://api.example.com is better than example.com, and https://api.example.com/billing is better still if you have multiple APIs under the same domain.
3. Ignoring the array case
The aud claim can be either a single string or an array of strings. Many hand-rolled validation implementations only handle the string case and break silently when they receive an array, or vice versa. Some libraries handle this for you, but not all.
Use your JWT library's built-in audience validation to avoid this pitfall. Most mature libraries handle both formats correctly.
4. Accepting any value in a multi-audience token
When a token has multiple values in its aud array, your service should only check that its own identifier is present. But some implementations go further and blindly trust the token if any recognized service is in the array. This is subtly wrong in environments where different services have different privilege levels.
For example, if a token lists both your public API and your internal admin API as audiences, the public API should accept it. But if your admin API accepts it solely because it sees itself in the list, without checking whether the request context makes sense, you've expanded the token's effective scope.
5. Confusing aud with iss
It's worth stating explicitly: aud and iss are not interchangeable. The iss claim identifies who created the token. The aud claim identifies who should consume it. A token with iss: "https://auth.example.com" and aud: "https://api.example.com" was created by the auth server for the API server. Swapping these in your validation logic is a subtle bug that can be hard to catch in testing.
6. Using aud to encode roles or permissions
A less obvious but surprisingly common mistake is repurposing aud to carry authorization data. Instead of a service identifier, the claim gets populated with values like "admin" or "readonly":
This breaks audience validation entirely. Any service that checks whether it's the intended recipient will either fail (because "admin" doesn't match its identifier) or skip the check altogether, re-introducing the token replay problem.
Roles and permissions belong in dedicated claims like roles, scope, or custom claims in your token's namespace. The aud claim has one job (identifying the intended recipient) and overloading it creates ambiguity that's hard to debug and easy to exploit.
How aud interacts with other claims
The aud claim doesn't exist in isolation. It interacts with several other claims, and understanding these relationships is important for correct validation.
- aud + iss: These are complementary checks. The
issclaim confirms the token came from a trusted authorization server. Theaudclaim confirms the token was intended for your service. Both must pass. A token from a trusted issuer that's addressed to a different service is not valid for you. - aud + sub: The
subclaim identifies the user, but what the user is authorized to do depends on which service the token is for. A user might be an admin in Service A but a read-only user in Service B. If you accept a token intended for Service A (where they're an admin) in Service B, you might grant elevated privileges. Audience validation prevents this. - aud + scope/permissions: In OAuth 2.0 flows, access tokens often include a
scopeclaim alongsideaud. The scopes define what actions are permitted, and the audience defines where those actions are permitted. A token withscope: "read:users"andaud: "https://api.example.com"grants read access to users on that specific API. Without audience validation, those scopes could be exercised against any service that trusts the same issuer. - aud + azp: The
azp(authorized party) claim is occasionally used alongsideaudin OpenID Connect. When theaudarray contains multiple values,azpidentifies the specific client that requested the token. If your system usesazp, validate it in addition toaud, but never instead ofaud.
Best practices for aud claim validation
- Always validate the audience. Never skip
audvalidation, even in development or internal services. Use your JWT library's built-in audience check; don't roll your own string comparison. - Use specific audience values. Each service should have a unique audience identifier. Avoid generic values like organization names or wildcard domains.
- Handle both strings and arrays. The
audclaim can be either format per the spec. Use a library that handles both, or ensure your custom validation does. - Validate aud alongside iss. These two claims form the core trust check: the token came from who you expect and was intended for your service. Never validate one without the other.
- Don't derive authorization solely from audience membership. The
audclaim confirms your service is an intended recipient. It doesn't replace proper authorization checks on scopes, roles, or permissions. - Log and alert on audience mismatches. A token with a valid signature and trusted issuer but the wrong audience hitting your API is a strong signal of a token replay attempt. Don't just reject silently; log it.
Final thoughts
The aud claim is the part of JWT validation that answers a deceptively important question: "Was this token actually meant for me?" Without it, you're relying entirely on signature and issuer validation, which in a multi-service environment is like checking that a letter has a valid postmark but never reading the address on the envelope.
The fix isn't complicated. Validate the audience. Use specific identifiers. These are small engineering decisions that prevent entire categories of token replay and forwarding attacks.