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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +14 -0
- data/README.md +30 -0
- data/lib/jwk-loader.rb +2 -0
- data/lib/jwk_loader/config/config.rb +1 -1
- data/lib/jwk_loader/jwks.rb +2 -0
- data/lib/jwk_loader/jwks_uri_provider.rb +28 -8
- data/lib/jwk_loader/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd8e5e883d3fe957a51bb6b11cd3b024e76df71f20f640c18778d4bd84c983a4
|
|
4
|
+
data.tar.gz: e6d02926c9d98cc4bdf956c9aa07bd5ab9098c8acee3c054f622609365ae3ddd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41aad8101ea210f2f5a892b4374676671b465f5598e4889a680c3c46db414db04dee61f4bf4152b7600501e3cf7df8d7feea5514db9e0e0ab707e0a77932bd30
|
|
7
|
+
data.tar.gz: b9127234138694fff757f5813a9ac6f307f277ef181bd0007829d01bc8932e805349e5fa4def48de0ab2f8f7f077144441b366f112b1634c72dd207468e205b1
|
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
|
|
data/lib/jwk_loader/jwks.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/jwk_loader/version.rb
CHANGED
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.
|
|
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.
|
|
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:
|
|
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: []
|