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

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