Skip to content

fix: Expire the cached JWK Set used to validate signatures - #29

Draft
maikdijkstra wants to merge 1 commit into
mainfrom
fix/expire-cached-jwks
Draft

maikdijkstra wants to merge 1 commit into
mainfrom
fix/expire-cached-jwks

Conversation

@maikdijkstra

@maikdijkstra maikdijkstra commented Nov 11, 2026 •

Copy link
Copy Markdown

Summary

 Keypair.jwt_decode
   jwks: jwk_loader_cached
     cached_jwks(force: unknown kid)
-      Rails.cache.fetch('keypairs/Keypair/jwks')               # never expires
+      Rails.cache.fetch('keypairs/Keypair/jwks/v2')            # new key: old entries never expired
+        expires_in = min(5 minutes, first keypair expires_at - now)

Without an expiry, the cached JWK Set was only refreshed when a token had an unknown kid. Because the next keypair is published ahead of time, rotation rarely triggered that, so keypairs past their expires_at, or deleted after a compromise, kept validating signatures for as long as the cache store kept the entry.

The first expiry comes from the keypairs that keyset already loads (now valid_keypairs), so this adds no extra query.

Found during a JWT audit of drieam-lti, which will require this release instead of patching Keypair itself.

Evidence

  • Before: the new .cached_jwks specs fail, e.g.
    cache keyset while keypair is valid
    travel 11 minutes (keypair expired after 10)
    jwt_decode(token signed by expired keypair) → payload returned
    
    After: 105 examples, 0 failures → raises JWT::DecodeError. RuboCop is clean.

Merge Danger

Door: two-way

Code-only. Reverting falls back to the old cache key.

Blast Radius: signature validation

  • Database load: apps rebuild the cached JWK Set at least every 5 minutes. That's one query plus, at most, creating the next keypair, which keyset already does.

Keypair.cached_jwks cached the JWK Set used by jwt_decode without an
expiry. It was only refreshed when a token had an unknown kid, and the
next keypair is published ahead of time, so keypairs past their
expires_at (or deleted after a compromise) kept validating signatures
for as long as the cache store kept the entry.

Expire the entry when the first keypair expires, at most after 5
minutes, under a new cache key so entries without an expiry written by
earlier versions are ignored.
@maikdijkstra
maikdijkstra force-pushed the fix/expire-cached-jwks branch from 90c0ec2 to c736eee Compare November 11, 2026 14:09

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant