skip to content

Write and explain a custom audience validator (OAuth2TokenValidator<Jwt>) for the aud claim. Why is validating audience important?

level: seniorimportance: should knowfreq 47%

answer

  1. no built-in audience validator
  2. aud is a List<String> — use contains
  3. success() vs failure(OAuth2Error INVALID_TOKEN)
  4. confused-deputy / token-reuse defense
  5. add via DelegatingOAuth2TokenValidator

basics

~10 s

Implement 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 s

Spring 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 lines
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) {
        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

for a junior

Knows aud identifies the intended recipient and should be checked.

for a middle

Can implement the validator and wire it into the chain correctly.

for a senior

Articulates the confused-deputy/token-reuse threat and multivalue handling.

for a principal

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

context