skip to content

How do you compose multiple JWT validators (timestamp, issuer, audience) into a single decoder, and why is JwtValidators.createDefaultWithIssuer preferred over building the chain by hand?

level: middleimportance: must knowfreq 58%

answer

  1. DelegatingOAuth2TokenValidator aggregates all errors
  2. setJwtValidator REPLACES defaults
  3. createDefaultWithIssuer = timestamp + issuer
  4. start from defaults, then add custom
  5. JwtIssuerValidator checks iss exactly

basics

~20 s

Combine validators with DelegatingOAuth2TokenValidator, which runs each in turn and aggregates errors, then attach it via decoder.setJwtValidator(...). JwtValidators.createDefaultWithIssuer(issuer) is preferred because it bundles the timestamp validator plus issuer check so you don't accidentally drop the defaults.

solid answer

~30 s

Spring's `OAuth2TokenValidator<Jwt>` validators are composed with `DelegatingOAuth2TokenValidator<Jwt>`, which invokes each delegate and collects all errors into one `OAuth2TokenValidatorResult`. You attach the composite to a `NimbusJwtDecoder` via `setJwtValidator(...)`. The catch: `setJwtValidator` **replaces** the decoder's default validator, so if you pass only a custom audience validator you silently lose expiry and issuer checking. To avoid that, start from `JwtValidators.createDefaultWithIssuer(issuer)` — it returns a delegating validator containing `JwtTimestampValidator` (exp/nbf) and `JwtIssuerValidator` (iss) already wired — then delegate that together with your custom validators. This guarantees you keep the security-critical defaults and only add to them, rather than reimplementing (and possibly forgetting) them.

code

java · 18 lines
java
@Bean
JwtDecoder jwtDecoder(
        @Value("${app.issuer}") String issuer,
        @Value("${app.jwk-set-uri}") String jwkSetUri) {

    NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();

    // Keeps the security-critical defaults (exp/nbf) AND the issuer check...
    OAuth2TokenValidator<Jwt> withIssuer =
        JwtValidators.createDefaultWithIssuer(issuer);
    // ...then ADD a custom audience validator on top.
    OAuth2TokenValidator<Jwt> audience =
        new AudienceValidator("api.example.com");

    decoder.setJwtValidator(
        new DelegatingOAuth2TokenValidator<>(withIssuer, audience));
    return decoder;
}

go deeper

for a junior

Know that DelegatingOAuth2TokenValidator combines validators.

for a middle

Must know setJwtValidator replaces defaults and why createDefaultWithIssuer is the safe base.

for a senior

Explains aggregation semantics, forward-compat benefit, and issuer-uri auto-config equivalence.

for a principal

Designs validator strategy across reactive/servlet, multi-issuer, and future-proofing via default factories.

## The composition primitive Every JWT validation rule in Spring Security implements `OAuth2TokenValidator<Jwt>` (method `validate(Jwt) -> OAuth2TokenValidatorResult`). To run several rules as one, wrap them in **`DelegatingOAuth2TokenValidator<Jwt>`**. It takes a varargs/collection of validators and, on `validate`, calls each delegate, **aggregating every error** into a single `OAuth2TokenValidatorResult` (it does not short-circuit on the first failure — you get all reasons). Success only if all delegates succeed. ## Attaching to the decoder A `NimbusJwtDecoder` (the standard `JwtDecoder` implementation backed by Nimbus JOSE) decodes and signature-verifies the token, then runs its **validator**. You install one with: ```java decoder.setJwtValidator(OAuth2TokenValidator<Jwt> validator); ``` **Critical semantics:** this *replaces* whatever validator the decoder currently has. A freshly built `NimbusJwtDecoder` already has a default validator (`JwtValidators.createDefault()`, which includes `JwtTimestampValidator`). If you call `setJwtValidator(new AudienceValidator(...))` you **throw the defaults away** — no more expiry enforcement. This is the single most common mistake in this area. ## The safe recipe with JwtValidators `JwtValidators` is a factory of pre-composed default sets: - `JwtValidators.createDefault()` — a `DelegatingOAuth2TokenValidator` containing at least `JwtTimestampValidator`. - `JwtValidators.createDefaultWithIssuer(String issuer)` — the above **plus** a `JwtIssuerValidator` bound to your issuer. - (Newer Spring Security also offers `createDefaultWithValidators(List<OAuth2TokenValidator<Jwt>>)` to append your own to the defaults in one call.) So the idiomatic, safe chain is: ```java OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); OAuth2TokenValidator<Jwt> audience = new AudienceValidator("api.example.com"); decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(withIssuer, audience)); ``` Here `withIssuer` itself is a delegating validator; nesting delegating validators is fine — the tree is flattened logically at validate time. ## JwtIssuerValidator `JwtIssuerValidator(String issuer)` checks the `iss` claim equals the expected issuer string exactly. This defends against tokens minted by a different (possibly attacker-controlled) authorization server. When you configure `issuer-uri` in Boot, this validator is added for you and the issuer metadata is also fetched to locate the JWK set — but if you build the decoder manually you must add it yourself, which is exactly why `createDefaultWithIssuer` exists. ## Why factory over hand-rolling - **Correctness:** you can't forget the timestamp validator. - **Forward-compat:** if Spring adds new default checks in a future version, `createDefault*` picks them up automatically; a hand-built list won't. - **Less code / clearer intent.** ## Gotchas - `setJwtValidator` replaces — never assume it appends. - Order of delegates doesn't change the pass/fail outcome (all run, all errors aggregate), but is fine to keep logical (timestamp, issuer, audience). - The reactive stack mirrors this with `ReactiveJwtDecoder` / `setJwtValidator` and the same `DelegatingOAuth2TokenValidator`. - Auto-config already does the right thing for `issuer-uri`; only hand-build when you need custom validators like audience.

  • Does DelegatingOAuth2TokenValidator stop at the first failing validator?
    No. It runs every delegate and aggregates all their errors into one OAuth2TokenValidatorResult, so a caller can see every reason a token was rejected, not just the first.
  • What is the danger of calling setJwtValidator with only a custom validator?
    It replaces the decoder's default validator, so you lose timestamp (exp/nbf) and any issuer checking. Always compose your custom validator with JwtValidators.createDefault()/createDefaultWithIssuer via DelegatingOAuth2TokenValidator.

saying these in an interview costs you the question

  • Assuming setJwtValidator appends to existing validators
  • Building the chain by hand and forgetting the timestamp validator
  • Thinking the delegating validator short-circuits on first error
  • Confusing JwtIssuerValidator (iss) with signature/JWK verification

context