jwk-loader 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67a15a343d854ec41ad0a81e27bb56d1d31d4ffbeba011f07297fc97476f3527
4
- data.tar.gz: 5bf06d581feafa9d0e8596beea16cf09edd2589ec09cf7934d1451a706320843
3
+ metadata.gz: fd8e5e883d3fe957a51bb6b11cd3b024e76df71f20f640c18778d4bd84c983a4
4
+ data.tar.gz: e6d02926c9d98cc4bdf956c9aa07bd5ab9098c8acee3c054f622609365ae3ddd
5
5
  SHA512:
6
- metadata.gz: ec4bbbeadaba81fb48a30958d3e1ee1393b82bb96094b995f2f2fdd85ce91cab0bbe39a361aa065b1da4f87a94ccfd954fd199b46d0a4339554bb53bc78487e6
7
- data.tar.gz: df1a43c04b157c052593e3bcb1acc12df6d219fe62f33af53652ee5d6ce49054b6a0d3605775ed41a8b22eb39bc3329a957e75d33a49feb26c3dbdde7770f745
6
+ metadata.gz: 41aad8101ea210f2f5a892b4374676671b465f5598e4889a680c3c46db414db04dee61f4bf4152b7600501e3cf7df8d7feea5514db9e0e0ab707e0a77932bd30
7
+ data.tar.gz: b9127234138694fff757f5813a9ac6f307f277ef181bd0007829d01bc8932e805349e5fa4def48de0ab2f8f7f077144441b366f112b1634c72dd207468e205b1
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "1.2.0"
2
+ ".": "1.3.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.0](https://github.com/anakinj/jwk-loader/compare/v1.2.0...v1.3.0) (2026-11-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * add optional maximum JWKS cache age ([#23](https://github.com/anakinj/jwk-loader/issues/23)) ([725482e](https://github.com/anakinj/jwk-loader/commit/725482e97cfb30ddc7792eb35d9f35fb792cba2d))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * handle absent cache entries during invalidation ([#18](https://github.com/anakinj/jwk-loader/issues/18)) ([0942d81](https://github.com/anakinj/jwk-loader/commit/0942d81b924928687e7408c85a9378ac9427af45))
14
+ * preserve cached JWKS when refreshing fails ([#17](https://github.com/anakinj/jwk-loader/issues/17)) ([cd4e68b](https://github.com/anakinj/jwk-loader/commit/cd4e68b6f4f6c01ef5aa7b1be430da4df606a0e8))
15
+ * reject unsuccessful JWKS HTTP responses ([#16](https://github.com/anakinj/jwk-loader/issues/16)) ([edf46b9](https://github.com/anakinj/jwk-loader/commit/edf46b92001c5348e51c309fa24523991b91275a))
16
+
3
17
  ## [1.2.0](https://github.com/anakinj/jwk-loader/compare/v1.1.0...v1.2.0) (2025-07-02)
4
18
 
5
19
 
data/README.md CHANGED
@@ -26,6 +26,20 @@ require "jwk-loader"
26
26
  JWT.decode(token, nil, true, algorithm: "RS512", jwks: JwkLoader.for_uri(uri: "https://url/to/public/jwks") )
27
27
  ```
28
28
 
29
+ ### Handling loading errors
30
+
31
+ Non-2xx JWKS responses raise native HTTP errors before JSON parsing; handle `Net::HTTPExceptions` alongside `JWT::DecodeError`. Invalid URIs, connection/TLS failures, timeouts, malformed JSON, and invalid keys propagate unchanged; examples include `URI::InvalidURIError`, `Net::OpenTimeout`, `JSON::ParserError`, or `JWT::JWKError`.
32
+
33
+ ```ruby
34
+ class AuthenticationError < StandardError; end
35
+
36
+ begin
37
+ JWT.decode(token, nil, true, algorithm: "RS512", jwks: JwkLoader.for_uri(uri: jwks_uri))
38
+ rescue Net::HTTPExceptions, JWT::DecodeError => error
39
+ raise AuthenticationError, "Token verification failed", cause: error
40
+ end
41
+ ```
42
+
29
43
  ### Testing endpoints protected by JWT tokens
30
44
 
31
45
  When testing HTTP endpoints protected by asymmetric JWT keys the mechanism in `jwk_loader/test` can be used to simplify the process.
@@ -49,12 +63,15 @@ end
49
63
 
50
64
  ### Configuring the gem
51
65
 
66
+ The default `cache_grace_period` is 900 seconds, measured from the last successful fetch. An unknown-key or explicit invalidation hint can refresh a cached set once that period has elapsed; it is a refresh cooldown, not a maximum lifetime for known keys. Refreshing leaves the previous cached set available until its replacement has loaded successfully.
67
+
52
68
  ```ruby
53
69
  require "jwt-loader"
54
70
 
55
71
  JwkLoader.configure do |config|
56
72
  config[:cache] = YetAnotherCache.new
57
73
  config[:cache_grace_period] = 999
74
+ config[:cache_max_age] = 600
58
75
  end
59
76
  ```
60
77
 
@@ -66,9 +83,22 @@ require "jwt-loader"
66
83
  JwkLoader.configure do |config|
67
84
  config.cache = YetAnotherCache.new
68
85
  config.cache_grace_period = 999
86
+ config.cache_max_age = 600
69
87
  end
70
88
  ```
71
89
 
90
+ `cache_max_age` optionally limits the lifetime of remotely cached sets, in seconds since the last successful fetch. It defaults to `nil` (no expiry). Once the age limit is reached, the next lookup fetches a replacement even for a known key and even within `cache_grace_period`. Expiry fails closed: if that fetch fails, the error propagates and expired keys are not returned. There is no background refresh or expired-key fallback. Use a finite non-negative number of seconds; `0` refreshes on every lookup. Locally supplied test keys are unaffected.
91
+
92
+ The maximum age can also be set per URI; an explicit `nil` disables a global limit for that provider:
93
+
94
+ ```ruby
95
+ JwkLoader.for_uri(uri: jwks_uri, cache_max_age: 300)
96
+ JwkLoader.for_uri(uri: jwks_uri, cache_max_age: nil)
97
+ ```
98
+
99
+ A failed refresh propagates the original loading exception for both `kid_not_found` and `invalidate` hints, without deleting the previous set or changing its timestamp. The triggering `JWT.decode` call fails with that exception. Subsequent ordinary lookups can still use preserved keys that have not expired. Since failures do not advance the timestamp, a later lookup requiring a refresh can retry; there is no separate failed-fetch cooldown.
100
+
101
+ Invalidation hints are safe when the cache entry is absent or has been evicted: the provider performs an ordinary lookup, using locally supplied test keys or fetching the URI when needed.
72
102
 
73
103
  ## Development
74
104
 
data/lib/jwk-loader.rb CHANGED
@@ -12,6 +12,7 @@ module JwkLoader
12
12
  def for_uri(**options)
13
13
  options[:cache] ||= config[:cache]
14
14
  options[:cache_grace_period] ||= config[:cache_grace_period]
15
+ options[:cache_max_age] = config[:cache_max_age] unless options.key?(:cache_max_age)
15
16
  JwksUriProvider.new(**options)
16
17
  end
17
18
 
@@ -27,6 +28,7 @@ module JwkLoader
27
28
  @config ||= JwkLoader::Config.new.tap do |cfg|
28
29
  cfg[:cache] = MemoryCache.new
29
30
  cfg[:cache_grace_period] = 900
31
+ cfg[:cache_max_age] = nil
30
32
  end
31
33
  end
32
34
 
@@ -13,7 +13,7 @@ module JwkLoader
13
13
  end
14
14
 
15
15
  def [](key)
16
- registry[key] || (raise ConfigurationNotFound, key)
16
+ registry.fetch(key) { raise ConfigurationNotFound, key }
17
17
  end
18
18
 
19
19
  def method_missing(name, *args)
@@ -11,6 +11,8 @@ module JwkLoader
11
11
  uri = URI.parse(uri) unless uri.is_a?(URI)
12
12
 
13
13
  response = Net::HTTP.get_response(uri)
14
+ response.value # Raises for non-2xx HTTP responses.
15
+
14
16
  from_json(response.body)
15
17
  end
16
18
 
@@ -2,33 +2,53 @@
2
2
 
3
3
  module JwkLoader
4
4
  class JwksUriProvider
5
- attr_reader :uri, :cache, :cache_grace_period
5
+ attr_reader :uri, :cache, :cache_grace_period, :cache_max_age
6
6
 
7
- def initialize(uri:, cache:, cache_grace_period:)
7
+ def initialize(uri:, cache:, cache_grace_period:, cache_max_age: nil)
8
8
  @uri = uri
9
9
  @cache = cache
10
10
  @cache_grace_period = cache_grace_period
11
+ @cache_max_age = cache_max_age
12
+ validate_cache_max_age!
11
13
  end
12
14
 
13
15
  def call(options)
14
- invalidate_cache! if options[:kid_not_found] || options[:invalidate]
15
- jwks
16
+ refreshed = refresh_cache! if options[:kid_not_found] || options[:invalidate]
17
+ refreshed || jwks
16
18
  end
17
19
 
18
20
  private
19
21
 
22
+ def validate_cache_max_age!
23
+ return if cache_max_age.nil?
24
+ return if cache_max_age.is_a?(Numeric) && cache_max_age.real? &&
25
+ cache_max_age.finite? && cache_max_age >= 0
26
+
27
+ raise ArgumentError, "cache_max_age must be nil or a finite non-negative number of seconds"
28
+ end
29
+
20
30
  def jwks
21
31
  from_cache || from_memory || from_uri
22
32
  end
23
33
 
24
- def invalidate_cache!
25
- return if Time.now - cache_entry.fetch(:fetched_at) < cache_grace_period
34
+ def refresh_cache!
35
+ entry = cache_entry
36
+ return if entry.nil? || Time.now - entry.fetch(:fetched_at) < cache_grace_period
26
37
 
27
- cache.delete(uri)
38
+ if (keys = from_memory)
39
+ cache.delete(uri)
40
+ keys
41
+ else
42
+ from_uri
43
+ end
28
44
  end
29
45
 
30
46
  def from_cache
31
- cache_entry&.fetch(:jwks)
47
+ entry = cache_entry
48
+ return if entry.nil?
49
+ return if cache_max_age && Time.now - entry.fetch(:fetched_at) >= cache_max_age
50
+
51
+ entry.fetch(:jwks)
32
52
  end
33
53
 
34
54
  def from_memory
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JwkLoader
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwk-loader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joakim Antman
@@ -75,7 +75,7 @@ metadata:
75
75
  allowed_push_host: https://rubygems.org
76
76
  homepage_uri: https://github.com/anakinj/jwk-loader
77
77
  source_code_uri: https://github.com/anakinj/jwk-loader
78
- changelog_uri: https://github.com/anakinj/jwk-loader/blob/1.2.0/CHANGELOG.md
78
+ changelog_uri: https://github.com/anakinj/jwk-loader/blob/1.3.0/CHANGELOG.md
79
79
  rubygems_mfa_required: 'true'
80
80
  rdoc_options: []
81
81
  require_paths:
@@ -91,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubygems_version: 3.6.7
94
+ rubygems_version: 4.0.16
95
95
  specification_version: 4
96
96
  summary: Tooling for handling JWK loading, parsing and caching
97
97
  test_files: []