Class: JWT::PQ::JWKSet::Loader

Inherits:
Object
  • Object
show all
Defined in:
lib/jwt/pq/jwks_loader.rb

Overview

HTTP-backed JWKS loader with TTL cache and ETag revalidation.

Fetches a remote JWKS document (e.g. /.well-known/jwks.json), parses it via import, and caches the result per-URL. Subsequent calls inside the TTL window return the cached set without touching the network. Once the TTL expires, an If-None-Match conditional GET is issued using the stored ETag; a 304 Not Modified response refreshes the cache timestamp without re-parsing.

Prefer the fetch shortcut over instantiating this class directly — it uses a process-global loader so the cache is shared across callers.

Defense-in-depth defaults:

  • HTTPS only (pass allow_http: true to override for development).
  • Redirects are rejected; update the URL to the canonical location.
  • Response body is capped at 1 MB (max_body_bytes); ML-DSA public keys are ~1.3–2.6 KB each, so 1 MB already allows several hundred rotation candidates.
  • Read/open timeouts default to 5 seconds.
  • Failures raise JWT::PQ::JWKSFetchError; parse errors on the fetched body surface as KeyError from import.

Examples:

Fetch and verify a token

jwks = JWT::PQ::JWKSet.fetch("https://issuer.example/.well-known/jwks.json")
_payload, header = JWT.decode(token, nil, false)
key = jwks[header["kid"]] or raise "unknown kid"
payload, = JWT.decode(token, key, true, algorithms: [header["alg"]])

Constant Summary collapse

DEFAULT_CACHE_TTL =
300
DEFAULT_TIMEOUT =
5
DEFAULT_OPEN_TIMEOUT =
5
DEFAULT_MAX_BODY_BYTES =
1_048_576

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeLoader

Returns a new instance of Loader.



60
61
62
63
# File 'lib/jwt/pq/jwks_loader.rb', line 60

def initialize
  @cache = {}
  @mutex = Mutex.new
end

Class Method Details

.defaultLoader

Returns a process-global loader whose cache is shared across all JWT::PQ::JWKSet.fetch callers.

Returns:



50
51
52
# File 'lib/jwt/pq/jwks_loader.rb', line 50

def self.default
  @default ||= new
end

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

Reset the process-global loader — mainly for tests.



56
57
58
# File 'lib/jwt/pq/jwks_loader.rb', line 56

def self.reset_default!
  @default = nil
end

Instance Method Details

#cached?(url) ⇒ 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.

Returns:

  • (Boolean)


120
121
122
# File 'lib/jwt/pq/jwks_loader.rb', line 120

def cached?(url)
  @mutex.synchronize { @cache.key?(url) }
end

#clearvoid

This method returns an undefined value.

Drop all cached entries.



115
116
117
# File 'lib/jwt/pq/jwks_loader.rb', line 115

def clear
  @mutex.synchronize { @cache.clear }
end

#fetch(url, cache_ttl: DEFAULT_CACHE_TTL, timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT, max_body_bytes: DEFAULT_MAX_BODY_BYTES, allow_http: false) ⇒ JWKSet

Fetch the JWKS at url, honouring the cache if the entry is still fresh.

URL provenance. The URL is used verbatim: Loader#fetch does not resolve DNS, inspect the target IP, or block private, link-local, or cloud-metadata addresses. Callers are responsible for ensuring the URL comes from a trusted source (e.g. a pinned issuer configuration, not untrusted user input) to avoid SSRF.

Parameters:

  • url (String)

    the absolute URL of the JWKS document.

  • cache_ttl (Integer) (defaults to: DEFAULT_CACHE_TTL)

    seconds the cached set is considered fresh. Default: 300.

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    read timeout in seconds. Default: 5.

  • open_timeout (Integer) (defaults to: DEFAULT_OPEN_TIMEOUT)

    connect timeout in seconds. Default: 5.

  • max_body_bytes (Integer) (defaults to: DEFAULT_MAX_BODY_BYTES)

    cap on response body size. Default: 1 MB.

  • allow_http (Boolean) (defaults to: false)

    allow plain http:// URLs. Default: false (strongly recommended for production).

Returns:

  • (JWKSet)

    the parsed set of verification keys.

Raises:

  • (JWKSFetchError)

    on network error, timeout, non-2xx response, oversized body, redirect, or non-HTTPS URL.

  • (KeyError)

    if the fetched body is not a valid JWKS.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/jwt/pq/jwks_loader.rb', line 87

def fetch(url, # rubocop:disable Metrics/ParameterLists
          cache_ttl: DEFAULT_CACHE_TTL,
          timeout: DEFAULT_TIMEOUT,
          open_timeout: DEFAULT_OPEN_TIMEOUT,
          max_body_bytes: DEFAULT_MAX_BODY_BYTES,
          allow_http: false)
  uri = validate_uri!(url, allow_http: allow_http)

  fresh = fresh_entry(url, cache_ttl)
  return fresh.jwks if fresh

  existing = @mutex.synchronize { @cache[url] }
  result = do_http_get(uri, existing&.etag, timeout, open_timeout, max_body_bytes)

  if result[:not_modified] && existing
    @mutex.synchronize { existing.fetched_at = now }
    existing.jwks
  else
    jwks = JWKSet.import(result[:body])
    @mutex.synchronize do
      @cache[url] = CacheEntry.new(jwks, result[:etag], now)
    end
    jwks
  end
end