Class: Verikloak::JwksCache

Inherits:
Object
  • Object
show all
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 → 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 ({"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 })

Examples:

Basic usage

cache = Verikloak::JwksCache.new(jwks_uri: "https://issuer.example.com/protocol/openid-connect/certs")
keys  = cache.fetch! # → Array<Hash> of JWKs

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from JwksCacheRevalidation

#force_fetch!, #ttl_expired?

Constructor Details

#initialize(jwks_uri:, connection: nil, allow_http: false) ⇒ JwksCache

Returns a new instance of JwksCache.

Parameters:

  • jwks_uri (String)

    HTTPS URL of the JWKs endpoint

  • connection (Faraday::Connection, nil) (defaults to: nil)

    Optional Faraday connection for HTTP requests

  • allow_http (Boolean) (defaults to: false)

    When false (default), raises on plain HTTP URIs. Set true for local development only.

Raises:

  • (JwksCacheError)

    if the URI is not an HTTP(S) URL or resolves to a private/internal address



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

#connectionFaraday::Connection (readonly)

Injected Faraday connection (for testing and shared config across the gem)

Returns:

  • (Faraday::Connection)


165
166
167
# File 'lib/verikloak/jwks_cache.rb', line 165

def connection
  @connection
end

Instance Method Details

#build_conditional_headersHash

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.

Returns:

  • (Hash)

    { 'If-None-Match' => etag } when present, otherwise {}.



197
198
199
# File 'lib/verikloak/jwks_cache.rb', line 197

def build_conditional_headers
  @etag ? { 'If-None-Match' => @etag } : {}
end

#cachedArray<Hash>?

Returns the last cached JWKs without performing a network request.

Returns:

  • (Array<Hash>, nil)

    cached keys, or nil if never fetched



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.

Parameters:

  • force (Boolean) (defaults to: false)

    When true, revalidates over HTTP even while Cache-Control: max-age freshness holds (the ETag conditional request still applies, so an unchanged key set costs only a 304).

Returns:

  • (Array<Hash>)

    the cached JWKs after fetch/revalidation

Raises:

  • (JwksCacheError)

    on HTTP failures, invalid JSON, invalid structure, or cache miss on 304



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_atTime?

Timestamp of the last successful fetch or revalidation.

Returns:

  • (Time, nil)


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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


173
174
175
# File 'lib/verikloak/jwks_cache.rb', line 173

def stale?
  @mutex.synchronize { !fresh_by_ttl_locked? }
end

#with_error_handlingObject

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.

Raises:



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.message}", 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.message}", code: 'jwks_fetch_failed')
end