JWKS and rotating signing keys

JWT Authentication Course · lesson 11 of 13 · 5 min read

How verifiers find the key, and how to change it without an outage.

Open this lesson in the learning hub

Key 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 kid header.
  • 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 kid becomes 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.