Error message
unexpected "aud" claim value
jose (JavaScript) — JWTClaimValidationFailed, code ERR_JWT_CLAIM_VALIDATION_FAILED
What it means
The signature verified, but the token's `aud` claim does not contain the value you passed as `audience` to `jwtVerify`. jose compares exact strings: the check passes when at least one value in the token's `aud` (a string or an array) equals one of the audiences you allow. The token is authentic — it was simply issued for a different recipient, so accepting it would let a token minted for one API be replayed against another.
Why it happens
The token was minted for a different API
commonOne authorization server often serves several resource servers. A client that obtained a token for the orders API and sends it to the billing API gets exactly this error — the check is doing its job. The `err.payload` property of the error shows the claims the token actually carries.
Same API, differently spelled identifier
commonAudience matching is byte-exact: `https://api.example.com` and `https://api.example.com/` (trailing slash), http vs https, or a logical name such as `api://orders` vs the URL of the API are all different values. Identity providers copy the identifier verbatim from their configuration, so the verifier must use the same spelling.
An ID token sent where an access token is expected
occasionalIn OpenID Connect the ID token's `aud` is the client ID of the application that logged the user in, not your API. Front ends that forward the ID token instead of the access token therefore fail the audience check of every API behind them.
The client did not ask for a specific audience
occasionalSeveral providers (Auth0, Okta custom authorization servers, Keycloak with audience mappers) only put your API in `aud` when the token request names it. Without the `audience` or RFC 8707 `resource` parameter the token is issued for a default audience such as the userinfo endpoint.
How to fix it
- 1.
Print what the token carries next to what you expect
Decode the claims without trusting them and compare `aud` with your `audience` option character by character. Fix whichever side is wrong — the verifier's option or the issuer's configuration — but keep the check.
js import { decodeJwt, jwtVerify } from "jose"; console.log("token aud:", decodeJwt(token).aud); // string or array of strings await jwtVerify(token, jwks, { algorithms: ["ES256"], issuer: "https://issuer.example.com", audience: "https://api.example.com/orders", // exact string match }); - 2.
Request a token for the API you are calling
When the token was issued for another audience, the fix belongs on the client: name the target API in the token request so the authorization server puts it into `aud`.
bash # OAuth 2.0 client credentials: ask the authorization server for a token # minted for the API you are about to call (Auth0 uses "audience", # RFC 8707 resource indicators use "resource"). curl -s https://issuer.example.com/oauth/token \ -d grant_type=client_credentials \ -d client_id="$CLIENT_ID" -d client_secret="$CLIENT_SECRET" \ -d audience=https://api.example.com/orders - 3.
Allow several identifiers explicitly, if they are all yours
`audience` accepts an array. List every identifier that legitimately names this API (for example a migration from a URL to a logical name), rather than removing the option — without `audience`, jose does not check `aud` at all.
js await jwtVerify(token, jwks, { algorithms: ["ES256"], audience: ["https://api.example.com/orders", "orders-api"], // any one must match });