Class: Verikloak::JwksCache
- Inherits:
-
Object
- Object
- Verikloak::JwksCache
- Includes:
- JwksCacheRevalidation
- 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→ returns304 Not Modifiedwhen unchanged. - TTL freshness via
Cache-Control: max-age→ avoids HTTP requests while fresh.
On a successful 200 OK, the cache:
- Parses the JWKs JSON (
{"keys":[...]}) and validates each JWK haskid,kty,n,e. - Stores the keys in-memory, records
ETag, and computes freshness fromCache-Control.
On a 304 Not Modified, the cache:
- Keeps existing keys and ETag, optionally updates TTL from new
Cache-Control, and refreshesfetched_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!(force: false) ⇒ 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.
Methods included from JwksCacheRevalidation
Constructor Details
#initialize(jwks_uri:, connection: nil, allow_http: false) ⇒ JwksCache
Returns a new instance of JwksCache.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/verikloak/jwks_cache.rb', line 100 def initialize(jwks_uri:, connection: nil, allow_http: false) clean_jwks_uri = SafeUrl.normalize(jwks_uri) unless clean_jwks_uri raise JwksCacheError.new('Invalid JWKs URI: must be a non-empty HTTP(S) URL', code: 'jwks_fetch_failed') end if SafeUrl.insecure_http?(clean_jwks_uri, allow_http: allow_http) 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)
165 166 167 |
# File 'lib/verikloak/jwks_cache.rb', line 165 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.
197 198 199 |
# File 'lib/verikloak/jwks_cache.rb', line 197 def build_conditional_headers @etag ? { 'If-None-Match' => @etag } : {} end |
#cached ⇒ Array<Hash>?
Returns the last cached JWKs without performing a network request.
153 154 155 |
# File 'lib/verikloak/jwks_cache.rb', line 153 def cached @mutex.synchronize { @cached_keys } end |
#fetch!(force: false) ⇒ 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.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/verikloak/jwks_cache.rb', line 136 def fetch!(force: false) @mutex.synchronize do return @cached_keys if !force && 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.
159 160 161 |
# File 'lib/verikloak/jwks_cache.rb', line 159 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.
204 205 206 |
# File 'lib/verikloak/jwks_cache.rb', line 204 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.
173 174 175 |
# File 'lib/verikloak/jwks_cache.rb', line 173 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.
180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/verikloak/jwks_cache.rb', line 180 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 |