Companion code for the follow-up article. The repository now holds two Maven
projects sharing one docs/ tree:
jwt-authentication/ the hand-written filter application (unchanged, moved)
oauth2-resource-server/ a resource server, a Keycloak compose, and a stub
issuer whose JWK Set can be mutated on command
The stub exists because Keycloak will not rotate a signing key at a chosen
second, report how many times its JWKS endpoint was fetched, or drop a key from
the published set on request - and the caching and rotation measurements need
all three. The Keycloak run confirms the same code path against a real issuer.
Findings captured under docs/output/, all from real runs:
* The default validator stack does not check aud. A token minted for another
service in the same realm is accepted.
* Spring Security builds its JWKSource with refreshAheadCache(false) and
rateLimited(false), overriding two of Nimbus's protective defaults, and
enables Nimbus caching only when NO Spring cache was supplied - so
supplying one removes the five-minute expiry.
* A key retired from the JWK Set stops being accepted at t+300s with the
default cache, and never with a Spring cache that has no TTL.
* 25 tokens carrying an unknown kid produce 25 JWKS fetches at the issuer,
through permitAll() endpoints included.
* A hyphenated client id in an authorities-claim-expression parses as
subtraction; the SpelEvaluationException is swallowed and logged at TRACE.
* A clientScopes key in a Keycloak realm import replaces the built-in scopes
rather than adding to them.
New docs chapters 12-18. README covers both projects. Existing docs and scripts
updated for the new paths; no docs/output/ file from the first article moved, so
links in the published article still resolve.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013f7f2XZXrQ6gW3RtZE187t
4.8 KiB
4.8 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.