Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.
Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
in 7.0, and both configuration classes moved into spring-security-config
- ClientSettings.requireProofKey flipped from false to true, on the authorization server
(1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
- requireProofKey(false) does not make PKCE optional for a public client; the code
verifier is that client's only authentication at the token endpoint
- MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called
Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
4.9 KiB
18 — Resource server checklist
The list for a Spring Security resource server specifically. For the token-minting side, see 10 — Production checklist.
Claims
audis validated. It is not by default. One property, one bean, or one wrapped validator — 12. Without it, any token from your realm works against any of your services.issuer-urimatches the issuer string byte for byte, from wherever the resource server runs. Trailing slashes count. PinKC_HOSTNAME— 17.- The clock skew is a decision. 60 seconds by default, in both directions. If your revocation story is short-lived tokens, the real worst case is the lifetime plus a minute — 12.
- You know whether your issuer emits
typ: at+jwt. The default stack refuses it — 12. setJwtValidatoris never called with a bare validator. It replaces the whole stack. Wrap withJwtValidators.createDefaultWithValidators— 13.- The default stack is pinned by a test, so an upgrade that moves it goes red rather than quiet — 13.
Authorities
- Roles actually arrive. Keycloak's live under
realm_access.rolesandresource_access.<client>.roles; the default converter reads neither — 14. - Hyphenated client ids in SpEL expressions are quoted.
['reports-api'], not[reports-api]. The failure is a silent empty authority list — 14. - You have either a
JwtAuthenticationConverterbean or the properties, not both. The bean silently disables the properties — 14. - Realm and client roles that share a name do not collide into one
ROLE_namespace in a way that grants something — 14.
JWKS and rotation
- If you supplied a Spring
Cache, it has a TTL. Supplying one disables Nimbus's five-minute cache; aConcurrentMapCachenever expires and a retired key stays trusted — 15. - The JWKS cache is not shared with anything else. A refresh calls
cache.invalidate(), which clears the whole cache — 15. - You know your revocation window. It is the cache TTL, not zero, and not the token lifetime — 15.
- Your issuer's rotation leaves a publish window long enough for every resource server to see the new key before it starts signing with it — 15.
- Someone is watching the JWKS endpoint's request rate. A rate that tracks request
volume rather than instance count means unknown-
kidtraffic is amplifying through you — 16. - You have decided about rate limiting. Spring Security disables Nimbus's. Restoring
it means building the
JWKSourceyourself and giving up discovery — 16. - You have decided about outage tolerance. It is off. When the cache expires and the issuer is unreachable, every request fails — 15.
Endpoints
/api/public/decoderis deleted. The diagnostic in this repository reveals your JWK Set URI and cache timings to anyone who can reach it.- You know
/.well-known/oauth-protected-resourceexists. Spring Security 7 publishes it, unauthenticated, without being asked — 12. - CSRF is disabled deliberately, because a stateless bearer-token API has no ambient credential to protect — and for no other reason — 04.
Should you build this at all
If your services are minting their own tokens for their own users, you do not need a
resource server and you do not need an identity provider; the hand-written filter in
jwt-authentication/ is less code and fewer moving parts. See
09 — manual filter vs resource server.
The resource server earns its complexity when tokens come from somewhere you do not control: several services trusting one issuer, an identity provider you did not write, key rotation that has to happen without redeploying anything. If that is not your situation, most of this document is a list of ways to get something wrong that you could simply not have.
If you also own the issuer, the matching list for that side is
authorization-server/10.