Class: Verikloak::JwksCache
- Inherits:
-
Object
- Object
- Verikloak::JwksCache
- Defined in:
- lib/verikloak/jwks_cache.rb
Overview
Caches and revalidates JSON Web Key Sets (JWKs) fetched from a remote endpoint.
This cache supports two HTTP cache mechanisms:
-
**ETag revalidation** via ‘If-None-Match` → returns `304 Not Modified` when unchanged.
-
**TTL freshness** via ‘Cache-Control: max-age` → avoids HTTP requests while fresh.
On a successful ‘200 OK`, the cache:
-
Parses the JWKs JSON (‘href="...">keys”:`) and validates each JWK has `kid`, `kty`, `n`, `e`.
-
Stores the keys in-memory, records ‘ETag`, and computes freshness from `Cache-Control`.
On a ‘304 Not Modified`, the cache:
-
Keeps existing keys and ETag, optionally updates TTL from new ‘Cache-Control`, and refreshes `fetched_at`.
Errors are raised as JwksCacheError with structured ‘code` values:
-
‘jwks_fetch_failed` (network/HTTP errors)
-
‘jwks_parse_failed` (invalid JSON / structure)
-
‘jwks_cache_miss` (304 received but nothing cached)
## Dependency Injection Pass a preconfigured ‘Faraday::Connection` via `connection:` to control timeouts, adapters, and shared headers (kept consistent with Discovery).
`JwksCache.new(jwks_uri: "...", connection: Faraday.new { |f| f.request :retry })`
Instance Attribute Summary collapse
-
#connection ⇒ Faraday::Connection
readonly
Injected Faraday connection (for testing and shared config across the gem).
Instance Method Summary collapse
-
#build_conditional_headers ⇒ Hash
private
Builds conditional headers for revalidation.
-
#cached ⇒ Array<Hash>?
Returns the last cached JWKs without performing a network request.
-
#fetch! ⇒ Array<Hash>
Fetches the JWKs and updates the in-memory cache.
-
#fetched_at ⇒ Time?
Timestamp of the last successful fetch or revalidation.
-
#fresh_by_ttl? ⇒ Boolean
private
True when cached keys are still fresh per ‘Cache-Control: max-age`.
-
#initialize(jwks_uri:, connection: nil) ⇒ JwksCache
constructor
A new instance of JwksCache.
-
#stale? ⇒ Boolean
Whether the cache is considered stale.
-
#with_error_handling ⇒ Object
private
Wraps network/parse errors into JwksCacheError with structured codes.
Constructor Details
#initialize(jwks_uri:, connection: nil) ⇒ JwksCache
Returns a new instance of JwksCache.
42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/verikloak/jwks_cache.rb', line 42 def initialize(jwks_uri:, connection: nil) unless jwks_uri.is_a?(String) && jwks_uri.strip.match?(%r{^https?://}) raise JwksCacheError.new('Invalid JWKs URI: must be a non-empty HTTP(S) URL', code: 'jwks_fetch_failed') end @jwks_uri = jwks_uri @connection = connection || Verikloak::HTTP.default_connection @cached_keys = nil @etag = nil @fetched_at = nil @max_age = nil @mutex = Mutex.new end |
Instance Attribute Details
#connection ⇒ Faraday::Connection (readonly)
Injected Faraday connection (for testing and shared config across the gem)
93 94 95 |
# File 'lib/verikloak/jwks_cache.rb', line 93 def connection @connection end |
Instance Method Details
#build_conditional_headers ⇒ Hash
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Builds conditional headers for revalidation.
125 126 127 |
# File 'lib/verikloak/jwks_cache.rb', line 125 def build_conditional_headers @etag ? { 'If-None-Match' => @etag } : {} end |
#cached ⇒ Array<Hash>?
Returns the last cached JWKs without performing a network request.
81 82 83 |
# File 'lib/verikloak/jwks_cache.rb', line 81 def cached @mutex.synchronize { @cached_keys } end |
#fetch! ⇒ Array<Hash>
Fetches the JWKs and updates the in-memory cache.
Performs an HTTP GET with ‘If-None-Match` when an ETag is present and handles:
-
200: parses/validates body, updates keys, ETag, TTL and ‘fetched_at`.
-
304: keeps cached keys, updates TTL from headers (if present), refreshes ‘fetched_at`.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/verikloak/jwks_cache.rb', line 64 def fetch! @mutex.synchronize do return @cached_keys if fresh_by_ttl_locked? with_error_handling do # Build conditional request headers (ETag-based) headers = build_conditional_headers # Perform HTTP GET request response = @connection.get(@jwks_uri, nil, headers) # Handle HTTP response according to status code handle_response(response) end end end |
#fetched_at ⇒ Time?
Timestamp of the last successful fetch or revalidation.
87 88 89 |
# File 'lib/verikloak/jwks_cache.rb', line 87 def fetched_at @mutex.synchronize { @fetched_at } end |
#fresh_by_ttl? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
True when cached keys are still fresh per ‘Cache-Control: max-age`.
132 133 134 |
# File 'lib/verikloak/jwks_cache.rb', line 132 def fresh_by_ttl? @mutex.synchronize { fresh_by_ttl_locked? } end |
#stale? ⇒ Boolean
Whether the cache is considered stale.
Uses ‘Cache-Control: max-age` semantics when available: returns `true` if `max-age` has elapsed or nothing is cached.
101 102 103 |
# File 'lib/verikloak/jwks_cache.rb', line 101 def stale? @mutex.synchronize { !fresh_by_ttl_locked? } end |
#with_error_handling ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Wraps network/parse errors into Verikloak::JwksCacheError with structured codes.
108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/verikloak/jwks_cache.rb', line 108 def with_error_handling yield rescue JwksCacheError raise rescue Faraday::ConnectionFailed, Faraday::TimeoutError raise JwksCacheError.new('Connection failed', code: 'jwks_fetch_failed') rescue Faraday::Error => e raise JwksCacheError.new("JWKs fetch failed: #{e.}", code: 'jwks_fetch_failed') rescue JSON::ParserError raise JwksCacheError.new('Response is not valid JSON', code: 'jwks_parse_failed') rescue StandardError => e raise JwksCacheError.new("Unexpected JWKs fetch error: #{e.}", code: 'jwks_fetch_failed') end |