By AndyPublished
JWT aud Claim: What the Audience Means and How to Validate It
aud (audience) claim identifies the recipients a JWT is intended for, usually the API or client application that should accept it. RFC 7519 §4.1.3 allows it to be a single string or an array of strings, and requires a recipient to reject the token if it does not find itself in that value. Audience validation is what stops a token issued for one service from being replayed against another that trusts the same identity provider. The signature cannot catch that, because both services accept the same issuer's keys. This guide covers the format, how aud differs from sub,iss and azp, and how to configure audience checks in jsonwebtoken, jose and PyJWT.JWT aud Meaning
RFC 7519 §4.1.3 defines aud as the recipients the JWT is intended for. Each principal that processes the token must identify itself with a value in the audience claim; if it does not, and the claim is present, it must reject the token. The values are case-sensitive strings, usually URIs or client IDs. Their meaning is application-specific: nothing in the spec says what your API's identifier should be, only that the issuer and the API must agree on it.
RFC 8725 §3.9 turns this into advice for issuers: when the same issuer produces tokens for more than one recipient, those tokens must carry an aud and recipients must check it. RFC 9068 makesaud required in JWT access tokens.
Why skipping the check is dangerous
Picture a company that uses one identity provider for a low-risk newsletter app and a payments API. Both trust the same issuer and fetch the same JWKS. If the payments API checks only the signature andiss, then anyone who can get a token for the newsletter app, including the newsletter app's own developers or an attacker who compromises it, can present that token to the payments API and it will be accepted. The token is genuine; it was simply minted for someone else. This token-substitution pattern is one of the reasons audience checks appear in every JWT security checklist, including the JWT security best practices on this site. The same reasoning applies to third-party identity: a token your users obtained to sign in to some other site that uses the same provider must not work on yours.
JWT aud Claim Example: String or Array
// Single audience (most common)
{
"iss": "https://login.example.com/",
"sub": "user_8412",
"aud": "https://api.example.com",
"exp": 1790284151
}
// Multiple audiences
{
"iss": "https://login.example.com/",
"sub": "user_8412",
"aud": ["https://api.example.com", "https://login.example.com/userinfo"],
"exp": 1790284151
}Both forms are valid, and code that reads aud must handle both. A hand-written check likepayload.aud === 'https://api.example.com' fails silently for the array form. Auth0, for example, issues an array when an access token is requested for your API together with theopenid scope, adding its /userinfo URL as a second audience. Libraries handle both forms: the check passes if any value in the token matches any value you configured.
To see which audience a token actually carries, paste it into the jwtdecode.app decoder; it runs in the browser and shows aud alongside a description of the claim.
JWT Audience Validation in Code
In jsonwebtoken and jose, audience is checked only if you pass the option. Leaving it out is the most common way audience validation ends up missing entirely. PyJWT is stricter: if the token has an aud and you configured none, it raises an error.
Node.js: jsonwebtoken
import jwt from 'jsonwebtoken';
const payload = jwt.verify(token, publicKeyPem, {
algorithms: ['RS256'],
issuer: 'https://login.example.com/',
audience: 'https://api.example.com', // or an array, or a RegExp
});
// Mismatch: JsonWebTokenError "jwt audience invalid. expected: https://api.example.com"Node.js: jose
import { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://login.example.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://login.example.com/',
audience: 'https://api.example.com', // or ['https://api.example.com', 'legacy-id']
algorithms: ['RS256'],
});
// Mismatch: code ERR_JWT_CLAIM_VALIDATION_FAILED, claim "aud", message 'unexpected "aud" claim value'Python: PyJWT
import jwt
payload = jwt.decode(
token,
public_key,
algorithms=["RS256"],
audience="https://api.example.com", # or a list of accepted values
issuer="https://login.example.com/",
)
# Mismatch: jwt.InvalidAudienceError: Audience doesn't match
# aud present, no audience argument: jwt.InvalidAudienceError: Invalid audience| Library | Option | Error on mismatch |
|---|---|---|
| jsonwebtoken | audience: string | RegExp | array | JsonWebTokenError: jwt audience invalid. expected: <value> |
| jose | audience: string | string[] | ERR_JWT_CLAIM_VALIDATION_FAILED: unexpected "aud" claim value |
| PyJWT | audience=str | list | InvalidAudienceError: Audience doesn't match (or "Invalid audience" when none was configured) |
options={"verify_aud": False}. That turns off the check that keeps other applications' tokens out of your API. Configure the correct audience instead.JWT aud vs sub
sub is who the token is about: the user, or the service in a client credentials flow. aud is who the token is for: the party that should accept it. A token for user 8412 to call the orders API has sub: "user_8412" andaud: "https://orders.example.com". Your API authenticates the caller bysub only after aud has established that the token was meant for it.
JWT Audience vs Issuer
iss names who created and signed the token; aud names who should consume it. You need both checks. The issuer check (plus signature) says "this came from an identity provider I trust". The audience check says "and it was issued for me, not for some other application that uses the same provider". A multi-tenant provider adds a twist: iss may include the tenant, so check it exactly rather than by prefix.
| Claim | Answers | Example |
|---|---|---|
| iss | Who issued this token? | https://login.example.com/ |
| aud | Who is this token for? | https://api.example.com |
| sub | Who is this token about? | user_8412 |
| azp | Which client was it issued to? (OIDC) | spa-client-id |
| client_id | Which client requested it? (RFC 9068 access tokens) | spa-client-id |
JWT aud vs azp
azp (authorised party) comes from OpenID Connect Core, not RFC 7519. In an ID token it holds the client ID of the party the token was issued to. Normally the ID token's audience is that same client, so aud and azp carry the same value or azp is absent. They differ when one client obtains a token meant for another: Google's sign-in for mobile apps, for example, can issue an ID token whose aud is the backend's client ID whileazp is the Android or iOS app's client ID. The latest OIDC Core errata treatsazp as belonging to extensions and says clients may ignore it otherwise.
In access tokens, providers use azp to record which client obtained the token, whileaud names the API. Keycloak and Microsoft Entra ID (v2.0 tokens) both do this; RFC 9068 standardises the same idea as client_id. The practical rule: your API validatesaud to decide whether to accept the token, and may additionally checkazp or client_id if only certain client applications are allowed to call it. Never accept a token because azp matches while aud does not.
JWT Invalid Audience: Causes and Fixes
- ·ID token sent to an API. An ID token's
audis the client ID, not your API. The client should send the access token. See JWTs in OAuth 2.0 and OIDC. - ·No audience requested. Some providers only put your API in
audif the client asks for it: Auth0 needs theaudienceparameter on the authorisation request, and Entra ID needs a scope belonging to your API, such asapi://<app-id>/access. - ·Two spellings of the same API. Entra ID v2.0 access tokens always carry the API's client ID (a GUID) in
aud, while v1.0 tokens can carry the App ID URI instead. Accept exactly the values your API really receives. - ·Trailing slash or scheme mismatch.
https://api.example.comandhttps://api.example.com/are different strings. - ·Environment mix-up. A staging token presented to production carries the staging API identifier.
The wrong audience section of the errors guide lists the messages from other libraries and frameworks.
Keycloak JWT Audience
Keycloak is a frequent source of audience confusion. Out of the box, a user's access token often has"aud": "account", because the default realm roles include roles on Keycloak's ownaccount client, and Keycloak adds a client to aud when the token carries its roles. Your API's client ID appears in azp if it was the client that requested the token, but not necessarily in aud.
The fix is to add an Audience protocol mapper, either on the client or on a client scope assigned to it, with Included Client Audience set to your API's client (or Included Custom Audience set to an arbitrary identifier) and "Add to access token" enabled. Then configure your API to require that value. Resist the temptation to validate against account or to switch the check off; every client in the realm can obtain a token with that audience.
Choosing Audience Values
- ·Give each API (resource server) its own identifier. Sharing one audience across services means a token for any of them works on all of them.
- ·Use a stable URI such as
https://orders.example.com. It does not have to resolve; it only has to be unique and agreed. - ·Keep multi-audience tokens rare. Each extra audience is another service that can replay the token. For service-to-service calls, token exchange (RFC 8693) lets a service swap a token for one scoped to the next hop.
- ·Validate audience in every service, not just at the gateway, if services can be reached directly. The JWTs in microservices guide covers this layout.
Audience answers "is this token for me?". Whether the caller may perform a particular action is a separate question, answered by scopes or roles; see JWT scopes vs roles.
Summary
aud (RFC 7519 §4.1.3) names the intended recipients of a JWT, as a string or an array of strings, and a recipient must reject a token that does not list it. iss says who issued the token, sub who it is about, and azp or client_id which client obtained it. Always pass the audience option (audience in jsonwebtoken, jose and PyJWT), give each API a unique identifier, and fix invalid audience errors by requesting the right token rather than disabling the check.