Introduction

JWT tokens signed with RS256 use an asymmetric key pair: the identity provider signs tokens with a private key, and applications verify signatures using the corresponding public key. When the identity provider rotates its signing key, applications must fetch the updated public key from the JWKS (JSON Web Key Set) endpoint. If the application caches an old public key or the JWKS endpoint is unreachable, signature verification fails for all tokens signed with the new key.

Symptoms

  • All JWT-based authentication fails after the identity provider rotates its signing key
  • Application logs show JWT signature verification failed or invalid signature
  • Token decodes successfully but signature check fails
  • JWKS endpoint returns a different key ID (kid) than the token header
  • Error message: JwtException: JWT signature does not match locally computed signature

Common Causes

  • Application caching the JWKS response and not refreshing after key rotation
  • Identity provider rotated the signing key without maintaining the old key in JWKS
  • Application's JWKS cache TTL too long, serving stale public keys
  • Network connectivity issue preventing the application from reaching the JWKS endpoint
  • JWKS endpoint returning the wrong key set (e.g., for a different audience)

Step-by-Step Fix

  1. 1.Compare the token's key ID with the JWKS endpoint: Verify the key mismatch.
  2. 2.```bash
  3. 3.# Get the kid from the token header
  4. 4.echo "eyJhbGci..." | cut -d. -f1 | base64 -d | jq '.kid'
  5. 5.# Get the kid from the JWKS endpoint
  6. 6.curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys[].kid'
  7. 7.# They should match
  8. 8.`
  9. 9.Clear the JWKS cache to force a fresh fetch: Invalidate stale keys.
  10. 10.```java
  11. 11.// Spring Security - clear the JWK cache
  12. 12.// If using Nimbus JOSE+JWT:
  13. 13.JWKSetCache cache = new HTTPJWKSetCache(15, 30, TimeUnit.MINUTES);
  14. 14.// Reduce cache TTL to ensure fresh keys are fetched
  15. 15.`
  16. 16.Configure dynamic JWKS fetching with automatic refresh: Use a library that handles key rotation.
  17. 17.```java
  18. 18.// Spring Security OAuth2 Resource Server
  19. 19.@Bean
  20. 20.public JwtDecoder jwtDecoder() {
  21. 21.return NimbusJwtDecoder
  22. 22..withJwkSetUri("https://auth.example.com/.well-known/jwks.json")
  23. 23..jwsAlgorithm(RS256)
  24. 24..build();
  25. 25.}
  26. 26.// Spring automatically refreshes JWKS when kid mismatch is detected
  27. 27.`
  28. 28.Verify the identity provider is publishing both old and new keys: Ensure smooth rotation.
  29. 29.```bash
  30. 30.curl -s https://auth.example.com/.well-known/jwks.json | jq '.keys | length'
  31. 31.# Should return 2 or more during the rotation period
  32. 32.`
  33. 33.Test JWT verification with the updated keys: Confirm authentication works.
  34. 34.```bash
  35. 35.# Obtain a new token and test authentication
  36. 36.curl -H "Authorization: Bearer $NEW_JWT" https://api.example.com/protected
  37. 37.# Should return 200 OK
  38. 38.`

Prevention

  • Use JWKS libraries that automatically refresh on key ID mismatch
  • Set JWKS cache TTL to no more than 15 minutes
  • Ensure the identity provider publishes both old and new keys during rotation
  • Monitor JWT verification failure rates and alert on sudden increases
  • Test key rotation in staging before applying to production identity providers
  • Implement JWT verification fallback that attempts to re-fetch JWKS on first failure