1
0
Files
spring-security-demo/service-to-service/docs/04-what-is-not-validated.md

126 lines
4.9 KiB
Markdown

# 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) &middot; Next: [5. The gateway](05-the-gateway.md)*