Class: Verikloak::JwksCache

Inherits:
Object
  • Object
show all
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 })`

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

Constructor Details

#initialize(jwks_uri:, connection: nil) ⇒ 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

Raises:



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

#connectionFaraday::Connection (readonly)

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

Returns:

  • (Faraday::Connection)


93
94
95
# File 'lib/verikloak/jwks_cache.rb', line 93

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 `{}`.



125
126
127
# File 'lib/verikloak/jwks_cache.rb', line 125

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



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`.

Returns:

  • (Array<Hash>)

    the cached JWKs after fetch/revalidation

Raises:

  • (JwksCacheError)

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



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

Timestamp of the last successful fetch or revalidation.

Returns:

  • (Time, nil)


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`.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


101
102
103
# File 'lib/verikloak/jwks_cache.rb', line 101

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:



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.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