JWKS and rotating signing keys
How verifiers find the key, and how to change it without an outage.
Open this lesson in the learning hubKey points
- The issuer publishes its public keys at a JWKS endpoint. Verifiers fetch it, and each token names the key it was signed with in the
kidheader. - That indirection is what makes rotation possible: publish the new key first, wait for verifiers to see it, then start signing with it.
- Cache the JWKS. Fetching it per request is a denial of service against your own identity provider and adds a network hop to every call.
- But refresh on an unknown kid. A cache that never refreshes breaks every request the moment a key rotates - the classic self-inflicted outage.
- Rate-limit that refresh. Without a limit, a flood of tokens with a bogus
kidbecomes an amplification attack against the JWKS endpoint. - Keep the old key published for at least the maximum token lifetime after rotating. Removing it immediately invalidates every token still in flight.
Example
# The discovery document points at the keys.
$ curl https://auth.example.com/.well-known/openid-configuration
{
"issuer": "https://auth.example.com",
"jwks_uri": "https://auth.example.com/.well-known/jwks.json",
"id_token_signing_alg_values_supported": ["RS256"]
}
$ curl https://auth.example.com/.well-known/jwks.json
{ "keys": [
{ "kid": "2026-08-key", "kty": "RSA", "alg": "RS256", "use": "sig",
"n": "0vx7ag...", "e": "AQAB" },
{ "kid": "2026-05-key", "kty": "RSA", "alg": "RS256", "use": "sig",
"n": "sXchDa...", "e": "AQAB" }
] }
# TWO keys: the new one and the outgoing one. Both must be present
# during the overlap or in-flight tokens fail.
# The token names which one signed it:
# header = { "alg": "RS256", "typ": "JWT", "kid": "2026-08-key" }
---
# ROTATION, safely:
#
# day 0 publish the new key alongside the old. Sign with the OLD.
# day 1 verifier caches have picked up both (cache TTL has elapsed).
# day 1 start signing with the NEW key.
# day 2+ after max token lifetime has passed, remove the old key.
#
# Doing steps 1 and 3 together is the outage: verifiers still holding a
# cached JWKS without the new kid reject every token.
---
# Client side - cache, but refresh on an unknown kid, with a rate limit.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com
# Spring caches the JWK set and refreshes on an unknown kid automatically,
# with its own rate limiting. Hand-rolled verifiers usually get this wrong.
Publish the new key before you sign with it and keep the old one for a full token lifetime - otherwise rotation is an outage.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JWT Authentication Course course, and every lesson in it is listed on the JWT Authentication Course contents page.