Write and explain a custom audience validator (OAuth2TokenValidator<Jwt>) for the aud claim. Why is validating audience important?
answer
- no built-in audience validator
- aud is a List<String> — use contains
- success() vs failure(OAuth2Error INVALID_TOKEN)
- confused-deputy / token-reuse defense
- add via DelegatingOAuth2TokenValidator
basics
~10 sImplement OAuth2TokenValidator<Jwt>, check that jwt.getAudience() contains your API's identifier, and return OAuth2TokenValidatorResult.success() or failure with an OAuth2Error. It matters because it stops a token minted for another API from being accepted by yours.
solid answer
~40 sSpring has no built-in audience validator, so you implement `OAuth2TokenValidator<Jwt>`. In `validate(Jwt jwt)` you inspect `jwt.getAudience()` (a `List<String>` from the `aud` claim) and confirm it contains your resource server's identifier. If present, return `OAuth2TokenValidatorResult.success()`; otherwise return `failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN, "required audience missing", null))`. You then add it to the decoder's validator chain via `DelegatingOAuth2TokenValidator`, alongside the timestamp and issuer defaults. Audience validation is a **confused-deputy / token-reuse defense**: a valid, correctly-signed token issued for API-A must not be replayed against API-B. Without an `aud` check, any token from the same trusted issuer would be accepted by every service, letting a token leak from a low-value service authorize a high-value one. It enforces that a token is spent only where it was intended.
code
java · 21 linespublic final class AudienceValidator implements OAuth2TokenValidator<Jwt> {
private final String requiredAudience;
public AudienceValidator(String requiredAudience) {
this.requiredAudience = requiredAudience;
}
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
List<String> aud = jwt.getAudience();
if (aud != null && aud.contains(requiredAudience)) {
return OAuth2TokenValidatorResult.success();
}
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_TOKEN,
"The required audience '" + requiredAudience + "' is missing",
null);
return OAuth2TokenValidatorResult.failure(error);
}
}go deeper
Knows aud identifies the intended recipient and should be checked.
Can implement the validator and wire it into the chain correctly.
Articulates the confused-deputy/token-reuse threat and multivalue handling.
Sets org-wide policy for audience identifiers, multi-audience tokens, and consistency across services and reactive/servlet stacks.
## Why there's no built-in one Spring Security ships `JwtTimestampValidator` and `JwtIssuerValidator` but **no** audience validator, because the correct `aud` value is application-specific (it's *your* API's identifier). So audience validation is the canonical example of writing a custom `OAuth2TokenValidator<Jwt>`. ## The `aud` claim `aud` (audience) identifies the recipient(s) the token is intended for. It may be a single string or an array; in Spring the decoded `Jwt` normalizes it to `List<String> jwt.getAudience()`. The authorization server sets `aud` to the resource server(s) that should accept the token — often an API identifier/URI like `https://api.example.com` or a client/resource id. ## The interface ```java public interface OAuth2TokenValidator<T extends OAuth2Token> { OAuth2TokenValidatorResult validate(T token); } ``` `OAuth2TokenValidatorResult` is created via `success()` or `failure(OAuth2Error... errors)`. An `OAuth2Error` carries an error code (use `OAuth2ErrorCodes.INVALID_TOKEN`), a description, and an optional URI. ## Full implementation ```java public final class AudienceValidator implements OAuth2TokenValidator<Jwt> { private final String requiredAudience; public AudienceValidator(String requiredAudience) { this.requiredAudience = requiredAudience; } @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { if (jwt.getAudience() != null && jwt.getAudience().contains(requiredAudience)) { return OAuth2TokenValidatorResult.success(); } OAuth2Error error = new OAuth2Error( OAuth2ErrorCodes.INVALID_TOKEN, "The required audience " + requiredAudience + " is missing", null); return OAuth2TokenValidatorResult.failure(error); } } ``` ## Wiring it in ```java OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer); OAuth2TokenValidator<Jwt> audience = new AudienceValidator("https://api.example.com"); decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, audience)); ``` Because you start from `createDefaultWithIssuer`, you keep exp/nbf and issuer checks and merely add audience. ## Why audience validation matters (the security argument) Signature + issuer checks only prove *who minted the token* and *that it isn't tampered/expired* — not *who it was for*. In an ecosystem with many services trusting one authorization server, a token minted for a low-sensitivity service is signed by the same key as one for a high-sensitivity service. If the high-value service doesn't verify `aud`, a leaked/misdirected token from the low-value service would be accepted — a **confused-deputy** style escalation and token-reuse hole. Enforcing `aud` binds a token to its intended recipient. This is why OAuth 2.0 security BCP and OIDC recommend audience restriction, and resource-server hardening guides call for it explicitly. ## Gotchas / edge cases - **`aud` can be multivalued** — use `contains`, don't compare with `equals` on a String. - **Null/absent `aud`** — decide policy; typically treat missing audience as failure for a protected API. - **Case/format** — match the exact identifier the AS issues (URI vs. bare id); mismatch is a common misconfig. - **Don't skip issuer just because you check aud** — they defend different things (source vs. destination). - **Reactive stack** — same interface; the decoder is `ReactiveJwtDecoder` but the validator is identical. - Keep the validator stateless and thread-safe (it's a singleton bean-adjacent object invoked concurrently).
- Why compare with contains() rather than equals() against jwt.getAudience()?Because the aud claim can be an array of multiple audiences; Spring normalizes it to List<String>. equals against a single String would fail (or throw) whenever the token legitimately targets multiple recipients.
- If a token passes signature and issuer checks but lacks the correct aud, is it safe to accept?No. Signature/issuer prove origin and integrity, not intended recipient. Accepting it enables token reuse across services (confused-deputy). Audience validation is a distinct, necessary check.
saying these in an interview costs you the question
- Assuming Spring provides a built-in JwtAudienceValidator
- Comparing aud with equals on a String instead of contains on the list
- Claiming issuer validation makes audience validation redundant
- Returning success() when aud is null/missing for a protected API