# 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..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)