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
83 lines
4.8 KiB
Markdown
83 lines
4.8 KiB
Markdown
# 18 — Resource server checklist
|
|
|
|
[← Keycloak setup](17-keycloak-setup.md) · [README](../README.md)
|
|
|
|
The list for a Spring Security resource server specifically. For the token-minting side, see
|
|
[10 — Production checklist](10-production-checklist.md).
|
|
|
|
## Claims
|
|
|
|
- [ ] **`aud` is validated.** It is not by default. One property, one bean, or one wrapped
|
|
validator — [12](12-issuer-and-audience.md). Without it, any token from your realm
|
|
works against any of your services.
|
|
- [ ] **`issuer-uri` matches the issuer string byte for byte**, from wherever the resource
|
|
server runs. Trailing slashes count. Pin `KC_HOSTNAME` — [17](17-keycloak-setup.md).
|
|
- [ ] **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](12-issuer-and-audience.md).
|
|
- [ ] **You know whether your issuer emits `typ: at+jwt`.** The default stack refuses it —
|
|
[12](12-issuer-and-audience.md).
|
|
- [ ] **`setJwtValidator` is never called with a bare validator.** It replaces the whole
|
|
stack. Wrap with `JwtValidators.createDefaultWithValidators` — [13](13-validator-stack.md).
|
|
- [ ] **The default stack is pinned by a test**, so an upgrade that moves it goes red rather
|
|
than quiet — [13](13-validator-stack.md).
|
|
|
|
## Authorities
|
|
|
|
- [ ] **Roles actually arrive.** Keycloak's live under `realm_access.roles` and
|
|
`resource_access.<client>.roles`; the default converter reads neither —
|
|
[14](14-authentication-converter.md).
|
|
- [ ] **Hyphenated client ids in SpEL expressions are quoted.** `['reports-api']`, not
|
|
`[reports-api]`. The failure is a silent empty authority list — [14](14-authentication-converter.md).
|
|
- [ ] **You have either a `JwtAuthenticationConverter` bean or the properties, not both.**
|
|
The bean silently disables the properties — [14](14-authentication-converter.md).
|
|
- [ ] **Realm and client roles that share a name do not collide** into one `ROLE_`
|
|
namespace in a way that grants something — [14](14-authentication-converter.md).
|
|
|
|
## JWKS and rotation
|
|
|
|
- [ ] **If you supplied a Spring `Cache`, it has a TTL.** Supplying one disables Nimbus's
|
|
five-minute cache; a `ConcurrentMapCache` never expires and a retired key stays
|
|
trusted — [15](15-jwks-caching-and-rotation.md).
|
|
- [ ] **The JWKS cache is not shared with anything else.** A refresh calls
|
|
`cache.invalidate()`, which clears the whole cache — [15](15-jwks-caching-and-rotation.md).
|
|
- [ ] **You know your revocation window.** It is the cache TTL, not zero, and not the token
|
|
lifetime — [15](15-jwks-caching-and-rotation.md).
|
|
- [ ] **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](15-jwks-caching-and-rotation.md).
|
|
- [ ] **Someone is watching the JWKS endpoint's request rate.** A rate that tracks request
|
|
volume rather than instance count means unknown-`kid` traffic is amplifying through
|
|
you — [16](16-jwks-amplification.md).
|
|
- [ ] **You have decided about rate limiting.** Spring Security disables Nimbus's. Restoring
|
|
it means building the `JWKSource` yourself and giving up discovery —
|
|
[16](16-jwks-amplification.md).
|
|
- [ ] **You have decided about outage tolerance.** It is off. When the cache expires and the
|
|
issuer is unreachable, every request fails — [15](15-jwks-caching-and-rotation.md).
|
|
|
|
## Endpoints
|
|
|
|
- [ ] **`/api/public/decoder` is 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-resource` exists.** Spring Security 7
|
|
publishes it, unauthenticated, without being asked — [12](12-issuer-and-audience.md).
|
|
- [ ] **CSRF is disabled deliberately**, because a stateless bearer-token API has no
|
|
ambient credential to protect — and for no other reason
|
|
— [04](04-csrf-permitall-403.md).
|
|
|
|
## 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/`](../jwt-authentication) is less code and fewer moving parts. See
|
|
[09 — manual filter vs resource server](09-manual-filter-vs-resource-server.md).
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
[← Keycloak setup](17-keycloak-setup.md) · [README](../README.md)
|