# 4. What a resource server does not validate *Prev: [3. RestClient interceptors](03-restclient-interceptors.md) · Next: [5. The gateway](05-the-gateway.md)* This is the most important chapter in the module. ## The demonstration `reporting-service` is a client registered on the same authorization server, with nothing to do with the downstream service. Its access tokens carry `aud: reporting-api`. Send one to the downstream service, whose audience is `downstream-api`: ``` HTTP/1.1 200 { "sub": "reporting-service", "aud": ["reporting-api"], "scope": "[orders.read]", ... } ``` Two hundred. Full transcript: [`docs/output/03-audience-ignored.txt`](output/03-audience-ignored.txt). ## Why `JwtValidators.createDefault()` is a `DelegatingOAuth2TokenValidator` over three validators, read back by reflection in `ValidatorContractTests.defaultDelegates`: - `JwtTypeValidator` - `JwtTimestampValidator` - `X509CertificateThumbprintValidator` Structure, expiry, and certificate binding. **No issuer. No audience. No scope.** Setting `spring.security.oauth2.resourceserver.jwt.issuer-uri` gets you a `JwtIssuerValidator` on top of that, so the issuer is covered. Nothing anywhere adds an audience check unless you write it. The practical consequence: in an estate where several services trust one authorization server — which is the normal shape — **any token from that issuer is accepted by every service in it**. A token a partner integration obtained for the reporting API is a valid credential for the payments API. Scope may or may not save you; `scope: orders.read` did not, above. ## The fix, and its price ```java NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(jwks).build(); decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>( JwtValidators.createDefaultWithIssuer(issuer), new JwtAudienceValidator("downstream-api"))); ``` `JwtAudienceValidator` is a public class in 7.1. Two lines, and the demonstration above turns into `The aud claim is not valid`. The heavier option is the RFC 9068 profile: ```java JwtValidators.createAtJwtValidator() .issuer("http://127.0.0.1:9000") .audience("downstream-api") .build(); ``` That requires `typ`, `exp`, `sub`, `iat`, `jti`, `iss`, `aud` **and `client_id`** to all be present. It is stricter and it is also where two surprises live. ## Surprise 1: Spring Authorization Server does not emit an RFC 9068 token Out of the box its access tokens carry `typ: JWT` in the header and no `client_id` claim. Point `createAtJwtValidator()` at one and every token is rejected. Both are one line in an `OAuth2TokenCustomizer`: ```java context.getJwsHeader().type("at+jwt"); context.getClaims().claim("client_id", clientId); ``` ## Surprise 2: making the token compliant breaks every resource server that was not updated `NimbusJwtDecoder`'s default JOSE type verifier accepts `JWT` and an absent `typ`, and nothing else. The moment the authorization server starts typing tokens `at+jwt`, every service using Boot's auto-configured decoder answers: ``` WWW-Authenticate: Bearer error="invalid_token", error_description="An error occurred while attempting to decode the Jwt: the given typ value needs to be one of [JWT]" ``` [`docs/output/05-strict-validation.txt`](output/05-strict-validation.txt) has the downstream service accepting the new tokens and the edge service, one hop away and not updated, rejecting them. The message mentions neither RFC 9068 nor the authorization server. The fix on the receiving side: ```java NimbusJwtDecoder.withJwkSetUri(jwks) .jwtProcessorCustomizer((processor) -> processor.setJWSTypeVerifier( new DefaultJOSEObjectTypeVerifier<>(new JOSEObjectType("at+jwt"), JOSEObjectType.JWT, null))) .build(); ``` Roll that out **before** you change the authorization server, not after. ## A message worth recognising twice Two components produce almost the same sentence and mean opposite things: | Source | Message | |---|---| | Nimbus `DefaultJOSEObjectTypeVerifier` | `the given typ value needs to be one of [JWT]` | | Spring `JwtTypeValidator` (at+jwt profile) | `the given typ value needs to be one of [at+jwt, application/at+jwt]` | The first means "your authorization server is too modern for this decoder". The second means "your authorization server is not modern enough for this validator". ## And one thing that *is* on by default `X509CertificateThumbprintValidator` is in the default set. If a token carries a `cnf` claim with `x5t#S256` — a certificate-bound access token, RFC 8705 — Spring Security **already** checks it against the client certificate on the TLS connection, with no configuration. That is the strongest available defence against a stolen bearer token, it costs nothing on the resource server, and it needs mTLS to be terminated in the application rather than in a sidecar. Chapter 6. --- *Prev: [3. RestClient interceptors](03-restclient-interceptors.md) · Next: [5. The gateway](05-the-gateway.md)*