Class: Keycardai::OAuth::JWKSKeyring

Inherits:
Object
  • Object
show all
Defined in:
lib/keycardai/oauth/jwks_keyring.rb

Overview

Resolves and caches JWKS verification keys for an issuer (RFC 7517), so bearer-token verification on a hot path does not hit the network per request. Resolution is a lookup keyed by (issuer, kid): discover the issuer's jwks_uri (RFC 8414), fetch the key set, select the key matching the kid. Both steps are cached with a TTL; the key cache is bounded and evicts its oldest entry on overflow.

The cache is in-memory and local to this instance. Thread-safe: concurrent resolutions for the same issuer are de-duplicated so a burst of cold-cache requests triggers a single discovery and a single JWKS fetch.

Constant Summary collapse

DEFAULT_KEY_TTL =
300
DEFAULT_DISCOVERY_TTL =
3600
DEFAULT_FETCH_TIMEOUT =
10
MAX_CACHED_KEYS =
256

Instance Method Summary collapse

Constructor Details

#initialize(http_client: HTTP::NetHTTPClient.new, key_ttl: DEFAULT_KEY_TTL, discovery_ttl: DEFAULT_DISCOVERY_TTL, fetch_timeout: DEFAULT_FETCH_TIMEOUT, clock: -> { Time.now }) ⇒ JWKSKeyring

Returns a new instance of JWKSKeyring.

Parameters:

  • http_client (#get) (defaults to: HTTP::NetHTTPClient.new)

    pluggable transport (see HTTP::NetHTTPClient)

  • key_ttl (Numeric) (defaults to: DEFAULT_KEY_TTL)

    per-key cache lifetime in seconds

  • discovery_ttl (Numeric) (defaults to: DEFAULT_DISCOVERY_TTL)

    jwks_uri cache lifetime in seconds

  • fetch_timeout (Numeric) (defaults to: DEFAULT_FETCH_TIMEOUT)

    timeout for discovery and JWKS fetches

  • clock (#call) (defaults to: -> { Time.now })

    returns the current Time; override in tests



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/keycardai/oauth/jwks_keyring.rb', line 31

def initialize(http_client: HTTP::NetHTTPClient.new, key_ttl: DEFAULT_KEY_TTL,
               discovery_ttl: DEFAULT_DISCOVERY_TTL, fetch_timeout: DEFAULT_FETCH_TIMEOUT,
               clock: -> { Time.now })
  @http_client = http_client
  @key_ttl = key_ttl
  @discovery_ttl = discovery_ttl
  @fetch_timeout = fetch_timeout
  @clock = clock
  @keys = {}
  @discovery = {}
  @issuer_locks = {}
  @mutex = Mutex.new
end

Instance Method Details

#invalidate(issuer = nil) ⇒ void

This method returns an undefined value.

Drop cached keys and discovery results, for one issuer or all.

Parameters:

  • issuer (String, nil) (defaults to: nil)

    limit invalidation to this issuer



68
69
70
71
72
73
74
75
76
77
78
# File 'lib/keycardai/oauth/jwks_keyring.rb', line 68

def invalidate(issuer = nil)
  @mutex.synchronize do
    if issuer
      @keys.delete_if { |(cached_issuer, _), _| cached_issuer == issuer }
      @discovery.delete(issuer)
    else
      @keys.clear
      @discovery.clear
    end
  end
end

#key(issuer, kid) ⇒ OpenSSL::PKey::PKey

Resolve the verification key for a token's issuer and kid.

Parameters:

  • issuer (String)

    the token issuer URL

  • kid (String)

    the kid from the JWT header

Returns:

  • (OpenSSL::PKey::PKey)

    the verification key

Raises:



54
55
56
57
58
59
60
61
62
# File 'lib/keycardai/oauth/jwks_keyring.rb', line 54

def key(issuer, kid)
  cached = @mutex.synchronize { fresh_key(issuer, kid) }
  return cached if cached

  issuer_lock(issuer).synchronize do
    cached = @mutex.synchronize { fresh_key(issuer, kid) }
    cached || resolve(issuer, kid)
  end
end