Module: LcpRuby::Authentication::JwksCache
- Defined in:
- lib/lcp_ruby/authentication/jwks_cache.rb
Overview
Per-provider JWKS cache used by bearer validation. Lookup by (provider, kid); on miss, attempts a rate-limited refresh and re-looks up.
‘refresh!` is rate-limited to one HTTP fetch per minute per provider so a client crafting unknown-kid values cannot turn LCP into a JWKS-fetch amplifier. JWKS-level caching lives here rather than on `omniauth_openid_connect`’s internal cache because bearer validation needs explicit control over the rate limit.
Constant Summary collapse
- REFETCH_RATE_LIMIT =
seconds between forced refetches per provider
60
Class Method Summary collapse
- .clear! ⇒ Object
- .key_for(provider, kid:) ⇒ Object
-
.refresh!(provider) ⇒ Object
‘Concurrent::Map#compute` holds a per-key lock, so concurrent refreshes for the same provider serialise on one fetch (matters during key rotation: many requests miss together).
Class Method Details
.clear! ⇒ Object
62 63 64 |
# File 'lib/lcp_ruby/authentication/jwks_cache.rb', line 62 def clear! @cache = nil end |
.key_for(provider, kid:) ⇒ Object
21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/lcp_ruby/authentication/jwks_cache.rb', line 21 def key_for(provider, kid:) return nil if kid.nil? || kid.to_s.empty? entry = cache[provider.name] return entry[:keys][kid] if entry && entry[:keys].key?(kid) begin refresh!(provider) rescue ConfigurationError # Already stamped + logged via the rescue path; treat as miss. end cache[provider.name]&.dig(:keys, kid) end |
.refresh!(provider) ⇒ Object
‘Concurrent::Map#compute` holds a per-key lock, so concurrent refreshes for the same provider serialise on one fetch (matters during key rotation: many requests miss together). `last_refresh` is stamped on failure too, otherwise an IdP outage would let an adversary spamming unknown-kid tokens turn each request into a fetch (no rate limit because the cache stays empty).
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/lcp_ruby/authentication/jwks_cache.rb', line 41 def refresh!(provider) now = Time.now.to_i fetch_error = nil cache.compute(provider.name) do |existing| if existing && now - existing[:last_refresh] < REFETCH_RATE_LIMIT existing else begin { keys: build_keys_by_kid(fetch_jwks(provider)), last_refresh: now } rescue StandardError => e fetch_error = e (existing || { keys: {} }).merge(last_refresh: now) end end end raise fetch_error if fetch_error cache[provider.name] end |