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, allow_http: false) ⇒ 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, allow_http: false) ⇒ JwksCache
Returns a new instance of JwksCache.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/verikloak/jwks_cache.rb', line 46 def initialize(jwks_uri:, connection: nil, allow_http: false) unless jwks_uri.is_a?(String) raise JwksCacheError.new('Invalid JWKs URI: must be a non-empty HTTP(S) URL', code: 'jwks_fetch_failed') end clean_jwks_uri = jwks_uri.strip unless clean_jwks_uri.match?(%r{^https?://}) raise JwksCacheError.new('Invalid JWKs URI: must be a non-empty HTTP(S) URL', code: 'jwks_fetch_failed') end unless allow_http || clean_jwks_uri.start_with?('https://') raise JwksCacheError.new( 'JWKs URI must use HTTPS. Set allow_http: true to permit plain HTTP (development only).', code: 'insecure_jwks_uri' ) end validate_not_private!(clean_jwks_uri) unless allow_http @jwks_uri = clean_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)
112 113 114 |
# File 'lib/verikloak/jwks_cache.rb', line 112 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.
144 145 146 |
# File 'lib/verikloak/jwks_cache.rb', line 144 def build_conditional_headers @etag ? { 'If-None-Match' => @etag } : {} end |
#cached ⇒ Array<Hash>?
Returns the last cached JWKs without performing a network request.
100 101 102 |
# File 'lib/verikloak/jwks_cache.rb', line 100 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`.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/verikloak/jwks_cache.rb', line 83 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.
106 107 108 |
# File 'lib/verikloak/jwks_cache.rb', line 106 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`.
151 152 153 |
# File 'lib/verikloak/jwks_cache.rb', line 151 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.
120 121 122 |
# File 'lib/verikloak/jwks_cache.rb', line 120 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.
127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/verikloak/jwks_cache.rb', line 127 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 |