Class: JWT::PQ::JWKSet::Loader
- Inherits:
-
Object
- Object
- JWT::PQ::JWKSet::Loader
- 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: trueto 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.
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
-
.default ⇒ Loader
A process-global loader whose cache is shared across all fetch callers.
-
.reset_default! ⇒ Object
private
Reset the process-global loader — mainly for tests.
Instance Method Summary collapse
- #cached?(url) ⇒ Boolean private
-
#clear ⇒ void
Drop all cached entries.
-
#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. -
#initialize ⇒ Loader
constructor
A new instance of Loader.
Constructor Details
#initialize ⇒ Loader
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
.default ⇒ Loader
Returns a process-global loader whose cache is shared across all JWT::PQ::JWKSet.fetch callers.
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.
120 121 122 |
# File 'lib/jwt/pq/jwks_loader.rb', line 120 def cached?(url) @mutex.synchronize { @cache.key?(url) } end |
#clear ⇒ void
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.
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 |