ASVS 5.0 V10.3.1 requires that a resource server accepts only access tokens intended for its own use. The audience can be a structured claim such as aud in a JWT, or it can be checked through the token introspection endpoint. It is a Level 2 requirement.
Why it matters
Without an audience check, a token minted for a low risk API works against a sensitive one that trusts the same authorization server. A compromised or malicious service that receives a user's token can replay it elsewhere.
How to implement it
Give each API a resource identifier and require it.
import { jwtVerify, createRemoteJWKSet } from "jose";
const jwks = createRemoteJWKSet(new URL(JWKS_URL));
export async function checkAccessToken(token: string) {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
audience: "https://api.example.com/orders",
typ: "at+jwt",
algorithms: ["ES256"],
});
return payload;
}
For opaque tokens, call introspection (RFC 7662) and verify the response has active: true and that its aud matches your resource. Ask the authorization server to put your resource into the token using resource indicators (RFC 8707).
How to test it
Get an access token for a different API from the same server and call yours. Expect 401 (or 403 per your policy). Do the same with a token that has no aud.
Common mistakes
- Validating signature and expiry but not audience.
- Using the client ID as the audience of an access token by habit.
- Accepting any introspection response with
active: truewithout readingaud.
Quick check
How is audience checked for opaque tokens?
Through the introspection response, which should include an aud value matching the resource server.