ASVS 5.0 V9.2.3 requires that a service accepts only tokens intended for it. For JWTs, validate the aud claim against an allowlist defined in the service. It is a Level 2 requirement.
Why it matters
Suppose one identity provider serves your billing API and your reporting API with the same signing key. A user with a legitimate token for reporting can send it to billing. The signature is valid, the token is not expired, and it still must be refused, because it was never issued for billing.
How to implement it
Give every service an identifier, ask for it when requesting tokens, and require an exact match.
ALLOWED_AUDIENCES = ["https://billing.example.com"]
claims = jwt.decode(
token, key,
algorithms=["RS256"],
issuer="https://idp.example.com/",
audience=ALLOWED_AUDIENCES,
options={"require": ["exp", "aud"]},
)
The aud claim can be a string or an array, and a good library handles both. Require the claim, so a token with no aud does not slip through. Keep the allowlist in the service's own config, never derived from the token.
How to test it
Obtain a token for service A and call service B. Expect 401. Remove aud from a token (re-signed with a test key in a test environment) and expect 401. Send an array containing your audience plus another and expect acceptance only if your policy permits multi-audience tokens.
Common mistakes
- Setting
verify_aud=Falseto make a test pass. - Using a wildcard or prefix match for the audience.
- Using the same audience string for all services.
- Accepting a token because
audcontains your client ID when it is an ID token.
Quick check
The signature is valid and the token is unexpired. Why reject it?
Because the audience names another service. Valid tokens are still limited to the service they were issued for.