json-jwt 1.9.1 → 1.17.2

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
- SHA1:
3
- metadata.gz: 366867063d5d6443dc46c34adadd1a3fd4a7b574
4
- data.tar.gz: 8295a9c7c3620e1cb32d47e4b27f2e6fbf947790
2
+ SHA256:
3
+ metadata.gz: c53a91e5d33b3d3faa375c424a1aacba227456756da3eafe1794014ee69dae44
4
+ data.tar.gz: 5edb64703e8c0a0b91bceeff2d6313abbfbd86eee1623567cd2335e2cea7b1da
5
5
  SHA512:
6
- metadata.gz: 5ce3939943d51965ca03d2dd34c6c40623b2d337ca6b8206dcc90ae29cf0e450c205bd66191ad6bc29f70edcf3567d92ad30cc96ee20dec3b737bbae134960b4
7
- data.tar.gz: 7fdd1b34b2535b3fd905e46badd0d2cee536a071eb434bc145179c07772019823bff4fc2fc22eefebb40479570439e430cab9ad1e26849775cbfc45de2952bbb
6
+ metadata.gz: b1f8b09219b80b0e2350450522e53ead2dc344662617e517916063cc7a08f5c9dff5ba9d81f3ff3ebabadae708c71286db4b086d5093af0fb459cf1793ae3c28
7
+ data.tar.gz: da998f84ffaf2996b7ef02d9ad2f4aff6067ec14c352bb0f0f0811474506f00212171f8623363462b03dbe2f17832cb3873b9882dd70c5a136e4ba97e7b61217
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: nov
@@ -0,0 +1,31 @@
1
+ name: Spec
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ spec:
14
+ runs-on: ubuntu-latest
15
+ name: Ruby ${{ matrix.ruby }}
16
+ strategy:
17
+ matrix:
18
+ ruby:
19
+ - '3.2'
20
+ - '3.3'
21
+ - '3.4'
22
+ - '4.0'
23
+ steps:
24
+ - uses: actions/checkout@v3
25
+ - name: Set up Ruby
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ bundler-cache: true
30
+ - name: Run Specs
31
+ run: bundle exec rake spec
data/.gitmodules CHANGED
@@ -1,3 +1,3 @@
1
1
  [submodule "spec/helpers/json-jwt-nimbus"]
2
2
  path = spec/helpers/json-jwt-nimbus
3
- url = git://github.com/nov/json-jwt-nimbus.git
3
+ url = https://github.com/nov/json-jwt-nimbus.git
data/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.16.0] - 2022-10-08
4
+
5
+ ### Fixed
6
+
7
+ - Remove padding oracle by @btoews in https://github.com/nov/json-jwt/pull/109
8
+
9
+ ## [1.16.0] - 2022-10-08
10
+
11
+ ### Added
12
+
13
+ - start recording CHANGELOG
14
+
15
+ ### Changed
16
+
17
+ * Switch from httpclient to faraday v2 https://github.com/nov/json-jwt/pull/110
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  gemspec
data/README.md CHANGED
@@ -2,8 +2,6 @@
2
2
 
3
3
  JSON Web Token and its family (JSON Web Signature, JSON Web Encryption and JSON Web Key) in Ruby
4
4
 
5
- [![Build Status](https://secure.travis-ci.org/nov/json-jwt.png)](http://travis-ci.org/nov/json-jwt)
6
-
7
5
  ## Installation
8
6
 
9
7
  ```
@@ -49,6 +47,17 @@ input = "jwt_header.jwt_claims.jwt_signature"
49
47
  JSON::JWT.decode(input, public_key)
50
48
  ```
51
49
 
50
+ If you need to get a JWK from `jwks_uri` of OpenID Connect IdP, you can use `JSON::JWK::Set::Fetcher` to fetch (& optionally cache) it.
51
+
52
+ ```ruby
53
+ # JWK Set Fetching & Caching
54
+ # NOTE: Optionally by setting cache instance, JWKs are cached by kid.
55
+ JSON::JWK::Set::Fetcher.cache = Rails.cache
56
+
57
+ JSON::JWK::Set::Fetcher.fetch(jwks_uri, kid: kid)
58
+ # => returns JSON::JWK instance or raise JSON::JWK::Set::KidNotFound
59
+ ```
60
+
52
61
  For more details, read [Documentation Wiki](https://github.com/nov/json-jwt/wiki).
53
62
 
54
63
  ## Note on Patches/Pull Requests
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.9.1
1
+ 1.17.2
data/json-jwt.gemspec CHANGED
@@ -7,16 +7,21 @@ Gem::Specification.new do |gem|
7
7
  gem.summary = %q{JSON Web Token and its family (JSON Web Signature, JSON Web Encryption and JSON Web Key) in Ruby}
8
8
  gem.description = %q{JSON Web Token and its family (JSON Web Signature, JSON Web Encryption and JSON Web Key) in Ruby}
9
9
  gem.license = 'MIT'
10
- gem.files = `git ls-files`.split("\n")
11
- gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
10
+ gem.files = `git ls-files`.split("\n").reject do |f|
11
+ f.match(%r{^(test|spec|features)/})
12
+ end
12
13
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
14
  gem.require_paths = ['lib']
14
- gem.add_runtime_dependency 'url_safe_base64'
15
- gem.add_runtime_dependency 'activesupport'
15
+ gem.required_ruby_version = '>= 2.4'
16
+ gem.add_runtime_dependency 'activesupport', '>= 4.2'
17
+ gem.add_runtime_dependency 'base64'
16
18
  gem.add_runtime_dependency 'bindata'
17
- gem.add_runtime_dependency 'securecompare'
19
+ gem.add_runtime_dependency 'aes_key_wrap'
20
+ gem.add_runtime_dependency 'faraday', '~> 2.0'
21
+ gem.add_runtime_dependency 'faraday-follow_redirects'
18
22
  gem.add_development_dependency 'rake'
19
23
  gem.add_development_dependency 'simplecov'
24
+ gem.add_development_dependency 'webmock'
20
25
  gem.add_development_dependency 'rspec'
21
26
  gem.add_development_dependency 'rspec-its'
22
27
  end
data/lib/json/jose.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'securecompare'
1
+ require 'active_support/security_utils'
2
2
 
3
3
  module JSON
4
4
  module JOSE
@@ -6,11 +6,12 @@ module JSON
6
6
 
7
7
  included do
8
8
  extend ClassMethods
9
- include SecureCompare
10
9
  register_header_keys :alg, :jku, :jwk, :x5u, :x5t, :x5c, :kid, :typ, :cty, :crit
10
+
11
+ # NOTE: not used anymore in this gem, but keeping in case developers are calling it.
11
12
  alias_method :algorithm, :alg
12
13
 
13
- attr_accessor :header
14
+ attr_writer :header
14
15
  def header
15
16
  @header ||= {}
16
17
  end
@@ -25,14 +26,24 @@ module JSON
25
26
  when JSON::JWK
26
27
  key.to_key
27
28
  when JSON::JWK::Set
28
- key.detect do |jwk|
29
- jwk[:kid] && jwk[:kid] == kid
30
- end.try(:to_key) or raise JWK::Set::KidNotFound
29
+ key[kid]&.to_key or raise JWK::Set::KidNotFound
31
30
  else
32
31
  key
33
32
  end
34
33
  end
35
34
 
35
+ def secure_compare(a, b)
36
+ if ActiveSupport::SecurityUtils.respond_to?(:fixed_length_secure_compare)
37
+ begin
38
+ ActiveSupport::SecurityUtils.fixed_length_secure_compare(a, b)
39
+ rescue ArgumentError
40
+ false
41
+ end
42
+ else
43
+ ActiveSupport::SecurityUtils.secure_compare(a, b)
44
+ end
45
+ end
46
+
36
47
  module ClassMethods
37
48
  def register_header_keys(*keys)
38
49
  keys.each do |header_key|
@@ -45,15 +56,15 @@ module JSON
45
56
  end
46
57
  end
47
58
 
48
- def decode(input, key_or_secret = nil, algorithms = nil, encryption_methods = nil)
59
+ def decode(input, key_or_secret = nil, algorithms = nil, encryption_methods = nil, allow_blank_payload = false)
49
60
  if input.is_a? Hash
50
- decode_json_serialized input, key_or_secret, algorithms, encryption_methods
61
+ decode_json_serialized input, key_or_secret, algorithms, encryption_methods, allow_blank_payload
51
62
  else
52
- decode_compact_serialized input, key_or_secret, algorithms, encryption_methods
63
+ decode_compact_serialized input, key_or_secret, algorithms, encryption_methods, allow_blank_payload
53
64
  end
54
- rescue JSON::ParserError
65
+ rescue JSON::ParserError, ArgumentError
55
66
  raise JWT::InvalidFormat.new("Invalid JSON Format")
56
67
  end
57
68
  end
58
69
  end
59
- end
70
+ end
data/lib/json/jwe.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'securerandom'
2
2
  require 'bindata'
3
+ require 'aes_key_wrap'
3
4
 
4
5
  module JSON
5
6
  class JWE
@@ -13,9 +14,10 @@ module JSON
13
14
 
14
15
  attr_accessor(
15
16
  :public_key_or_secret, :private_key_or_secret,
16
- :plain_text, :cipher_text, :authentication_tag, :iv, :auth_data,
17
- :content_encryption_key, :jwe_encrypted_key, :encryption_key, :mac_key
17
+ :plain_text, :cipher_text, :iv, :auth_data,
18
+ :content_encryption_key, :encryption_key, :mac_key
18
19
  )
20
+ attr_writer :jwe_encrypted_key, :authentication_tag
19
21
 
20
22
  register_header_keys :enc, :epk, :zip, :apu, :apv
21
23
  alias_method :encryption_method, :enc
@@ -31,7 +33,7 @@ module JSON
31
33
  self.mac_key, self.encryption_key = derive_encryption_and_mac_keys
32
34
  cipher.key = encryption_key
33
35
  self.iv = cipher.random_iv # NOTE: 'iv' has to be set after 'key' for GCM
34
- self.auth_data = UrlSafeBase64.encode64 header.to_json
36
+ self.auth_data = Base64.urlsafe_encode64 header.to_json, padding: false
35
37
  cipher.auth_data = auth_data if gcm?
36
38
  self.cipher_text = cipher.update(plain_text) + cipher.final
37
39
  self
@@ -41,17 +43,29 @@ module JSON
41
43
  raise UnexpectedAlgorithm.new('Unexpected alg header') unless algorithms.blank? || Array(algorithms).include?(alg)
42
44
  raise UnexpectedAlgorithm.new('Unexpected enc header') unless encryption_methods.blank? || Array(encryption_methods).include?(enc)
43
45
  self.private_key_or_secret = with_jwk_support private_key_or_secret
44
- cipher.decrypt
45
46
  self.content_encryption_key = decrypt_content_encryption_key
46
47
  self.mac_key, self.encryption_key = derive_encryption_and_mac_keys
48
+
49
+ verify_cbc_authentication_tag! if cbc?
50
+
51
+ cipher.decrypt
47
52
  cipher.key = encryption_key
48
53
  cipher.iv = iv # NOTE: 'iv' has to be set after 'key' for GCM
49
54
  if gcm?
55
+ # https://github.com/ruby/openssl/issues/63
56
+ raise DecryptionFailed.new('Invalid authentication tag') if authentication_tag.length < 16
50
57
  cipher.auth_tag = authentication_tag
51
58
  cipher.auth_data = auth_data
52
59
  end
53
- self.plain_text = cipher.update(cipher_text) + cipher.final
54
- verify_cbc_authentication_tag! if cbc?
60
+
61
+ begin
62
+ self.plain_text = cipher.update(cipher_text) + cipher.final
63
+ rescue OpenSSL::OpenSSLError
64
+ # Ensure that the same error is raised for invalid PKCS7 padding
65
+ # as for invalid signatures. This prevents padding-oracle attacks.
66
+ raise DecryptionFailed
67
+ end
68
+
55
69
  self
56
70
  end
57
71
 
@@ -63,7 +77,7 @@ module JSON
63
77
  cipher_text,
64
78
  authentication_tag
65
79
  ].collect do |segment|
66
- UrlSafeBase64.encode64 segment.to_s
80
+ Base64.urlsafe_encode64 segment.to_s, padding: false
67
81
  end.join('.')
68
82
  end
69
83
 
@@ -71,21 +85,21 @@ module JSON
71
85
  case options[:syntax]
72
86
  when :general
73
87
  {
74
- protected: UrlSafeBase64.encode64(header.to_json),
88
+ protected: Base64.urlsafe_encode64(header.to_json, padding: false),
75
89
  recipients: [{
76
- encrypted_key: UrlSafeBase64.encode64(jwe_encrypted_key)
90
+ encrypted_key: Base64.urlsafe_encode64(jwe_encrypted_key, padding: false)
77
91
  }],
78
- iv: UrlSafeBase64.encode64(iv),
79
- ciphertext: UrlSafeBase64.encode64(cipher_text),
80
- tag: UrlSafeBase64.encode64(authentication_tag)
92
+ iv: Base64.urlsafe_encode64(iv, padding: false),
93
+ ciphertext: Base64.urlsafe_encode64(cipher_text, padding: false),
94
+ tag: Base64.urlsafe_encode64(authentication_tag, padding: false)
81
95
  }
82
96
  else
83
97
  {
84
- protected: UrlSafeBase64.encode64(header.to_json),
85
- encrypted_key: UrlSafeBase64.encode64(jwe_encrypted_key),
86
- iv: UrlSafeBase64.encode64(iv),
87
- ciphertext: UrlSafeBase64.encode64(cipher_text),
88
- tag: UrlSafeBase64.encode64(authentication_tag)
98
+ protected: Base64.urlsafe_encode64(header.to_json, padding: false),
99
+ encrypted_key: Base64.urlsafe_encode64(jwe_encrypted_key, padding: false),
100
+ iv: Base64.urlsafe_encode64(iv, padding: false),
101
+ ciphertext: Base64.urlsafe_encode64(cipher_text, padding: false),
102
+ tag: Base64.urlsafe_encode64(authentication_tag, padding: false)
89
103
  }
90
104
  end
91
105
  end
@@ -95,15 +109,15 @@ module JSON
95
109
  # common
96
110
 
97
111
  def gcm?
98
- [:A128GCM, :A256GCM].include? encryption_method.try(:to_sym)
112
+ [:A128GCM, :A256GCM].include? encryption_method&.to_sym
99
113
  end
100
114
 
101
115
  def cbc?
102
- [:'A128CBC-HS256', :'A256CBC-HS512'].include? encryption_method.try(:to_sym)
116
+ [:'A128CBC-HS256', :'A256CBC-HS512'].include? encryption_method&.to_sym
103
117
  end
104
118
 
105
119
  def dir?
106
- :dir == algorithm.try(:to_sym)
120
+ :dir == alg&.to_sym
107
121
  end
108
122
 
109
123
  def cipher
@@ -112,7 +126,7 @@ module JSON
112
126
  end
113
127
 
114
128
  def cipher_name
115
- case encryption_method.try(:to_sym)
129
+ case encryption_method&.to_sym
116
130
  when :A128GCM
117
131
  'aes-128-gcm'
118
132
  when :A256GCM
@@ -127,7 +141,7 @@ module JSON
127
141
  end
128
142
 
129
143
  def sha_size
130
- case encryption_method.try(:to_sym)
144
+ case encryption_method&.to_sym
131
145
  when :'A128CBC-HS256'
132
146
  256
133
147
  when :'A256CBC-HS512'
@@ -155,15 +169,13 @@ module JSON
155
169
  # encryption
156
170
 
157
171
  def jwe_encrypted_key
158
- @jwe_encrypted_key ||= case algorithm.try(:to_sym)
172
+ @jwe_encrypted_key ||= case alg&.to_sym
159
173
  when :RSA1_5
160
174
  public_key_or_secret.public_encrypt content_encryption_key
161
175
  when :'RSA-OAEP'
162
176
  public_key_or_secret.public_encrypt content_encryption_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
163
- when :A128KW
164
- raise NotImplementedError.new('A128KW not supported yet')
165
- when :A256KW
166
- raise NotImplementedError.new('A256KW not supported yet')
177
+ when :A128KW, :A256KW
178
+ AESKeyWrap.wrap content_encryption_key, public_key_or_secret
167
179
  when :dir
168
180
  ''
169
181
  when :'ECDH-ES'
@@ -209,15 +221,13 @@ module JSON
209
221
 
210
222
  def decrypt_content_encryption_key
211
223
  fake_content_encryption_key = generate_content_encryption_key # NOTE: do this always not to make timing difference
212
- case algorithm.try(:to_sym)
224
+ case alg&.to_sym
213
225
  when :RSA1_5
214
226
  private_key_or_secret.private_decrypt jwe_encrypted_key
215
227
  when :'RSA-OAEP'
216
228
  private_key_or_secret.private_decrypt jwe_encrypted_key, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING
217
- when :A128KW
218
- raise NotImplementedError.new('A128KW not supported yet')
219
- when :A256KW
220
- raise NotImplementedError.new('A256KW not supported yet')
229
+ when :A128KW, :A256KW
230
+ AESKeyWrap.unwrap jwe_encrypted_key, private_key_or_secret
221
231
  when :dir
222
232
  private_key_or_secret
223
233
  when :'ECDH-ES'
@@ -244,18 +254,25 @@ module JSON
244
254
  sha_digest, mac_key, secured_input
245
255
  )[0, sha_size / 2 / 8]
246
256
  unless secure_compare(authentication_tag, expected_authentication_tag)
247
- raise DecryptionFailed.new('Invalid authentication tag')
257
+ raise DecryptionFailed
248
258
  end
249
259
  end
250
260
 
251
261
  class << self
252
- def decode_compact_serialized(input, private_key_or_secret, algorithms = nil, encryption_methods = nil)
262
+ def decode_compact_serialized(input, private_key_or_secret, algorithms = nil, encryption_methods = nil, allow_blank_payload = false)
263
+ if allow_blank_payload
264
+ raise InvalidFormat.new("JWE w/ blank payload is not supported.")
265
+ end
253
266
  unless input.count('.') + 1 == NUM_OF_SEGMENTS
254
267
  raise InvalidFormat.new("Invalid JWE Format. JWE should include #{NUM_OF_SEGMENTS} segments.")
255
268
  end
256
269
  jwe = new
257
- _header_json_, jwe.jwe_encrypted_key, jwe.iv, jwe.cipher_text, jwe.authentication_tag = input.split('.').collect do |segment|
258
- UrlSafeBase64.decode64 segment
270
+ _header_json_, jwe.jwe_encrypted_key, jwe.iv, jwe.cipher_text, jwe.authentication_tag = input.split('.', NUM_OF_SEGMENTS).collect do |segment|
271
+ begin
272
+ Base64.urlsafe_decode64 segment
273
+ rescue ArgumentError
274
+ raise DecryptionFailed
275
+ end
259
276
  end
260
277
  jwe.auth_data = input.split('.').first
261
278
  jwe.header = JSON.parse(_header_json_).with_indifferent_access
@@ -265,7 +282,7 @@ module JSON
265
282
  jwe
266
283
  end
267
284
 
268
- def decode_json_serialized(input, private_key_or_secret, algorithms = nil, encryption_methods = nil)
285
+ def decode_json_serialized(input, private_key_or_secret, algorithms = nil, encryption_methods = nil, allow_blank_payload = false)
269
286
  input = input.with_indifferent_access
270
287
  jwe_encrypted_key = if input[:recipients].present?
271
288
  input[:recipients].first[:encrypted_key]
@@ -279,7 +296,7 @@ module JSON
279
296
  input[:ciphertext],
280
297
  input[:tag]
281
298
  ].join('.')
282
- decode_compact_serialized compact_serialized, private_key_or_secret, algorithms, encryption_methods
299
+ decode_compact_serialized compact_serialized, private_key_or_secret, algorithms, encryption_methods, allow_blank_payload
283
300
  end
284
301
  end
285
302
  end
@@ -5,17 +5,17 @@ module JSON
5
5
  def to_jwk(ex_params = {})
6
6
  params = {
7
7
  kty: :RSA,
8
- e: UrlSafeBase64.encode64(e.to_s(2)),
9
- n: UrlSafeBase64.encode64(n.to_s(2))
8
+ e: Base64.urlsafe_encode64(e.to_s(2), padding: false),
9
+ n: Base64.urlsafe_encode64(n.to_s(2), padding: false)
10
10
  }.merge ex_params
11
11
  if private?
12
12
  params.merge!(
13
- d: UrlSafeBase64.encode64(d.to_s(2)),
14
- p: UrlSafeBase64.encode64(p.to_s(2)),
15
- q: UrlSafeBase64.encode64(q.to_s(2)),
16
- dp: UrlSafeBase64.encode64(dmp1.to_s(2)),
17
- dq: UrlSafeBase64.encode64(dmq1.to_s(2)),
18
- qi: UrlSafeBase64.encode64(iqmp.to_s(2)),
13
+ d: Base64.urlsafe_encode64(d.to_s(2), padding: false),
14
+ p: Base64.urlsafe_encode64(p.to_s(2), padding: false),
15
+ q: Base64.urlsafe_encode64(q.to_s(2), padding: false),
16
+ dp: Base64.urlsafe_encode64(dmp1.to_s(2), padding: false),
17
+ dq: Base64.urlsafe_encode64(dmq1.to_s(2), padding: false),
18
+ qi: Base64.urlsafe_encode64(iqmp.to_s(2), padding: false),
19
19
  )
20
20
  end
21
21
  JWK.new params
@@ -27,10 +27,10 @@ module JSON
27
27
  params = {
28
28
  kty: :EC,
29
29
  crv: curve_name,
30
- x: UrlSafeBase64.encode64([coordinates[:x]].pack('H*')),
31
- y: UrlSafeBase64.encode64([coordinates[:y]].pack('H*'))
30
+ x: Base64.urlsafe_encode64([coordinates[:x]].pack('H*'), padding: false),
31
+ y: Base64.urlsafe_encode64([coordinates[:y]].pack('H*'), padding: false)
32
32
  }.merge ex_params
33
- params[:d] = UrlSafeBase64.encode64([coordinates[:d]].pack('H*')) if private_key?
33
+ params[:d] = Base64.urlsafe_encode64([coordinates[:d]].pack('H*'), padding: false) if private_key?
34
34
  JWK.new params
35
35
  end
36
36
 
@@ -44,6 +44,8 @@ module JSON
44
44
  :'P-384'
45
45
  when 'secp521r1'
46
46
  :'P-521'
47
+ when 'secp256k1'
48
+ :secp256k1
47
49
  else
48
50
  raise UnknownAlgorithm.new('Unknown EC Curve')
49
51
  end
@@ -0,0 +1,93 @@
1
+ module JSON
2
+ class JWK
3
+ class Set
4
+ module Fetcher
5
+ class UnexpectedFormat < JWT::Exception; end
6
+
7
+ class Cache
8
+ def fetch(cache_key, options = {})
9
+ yield
10
+ end
11
+
12
+ def delete(cache_key, options = {}); end
13
+ end
14
+
15
+ def self.logger
16
+ @@logger
17
+ end
18
+ def self.logger=(logger)
19
+ @@logger = logger
20
+ end
21
+ self.logger = Logger.new(STDOUT)
22
+ self.logger.progname = 'JSON::JWK::Set::Fetcher'
23
+
24
+ def self.debugging?
25
+ @@debugging
26
+ end
27
+ def self.debugging=(boolean)
28
+ @@debugging = boolean
29
+ end
30
+ def self.debug!
31
+ self.debugging = true
32
+ end
33
+ def self.debug(&block)
34
+ original = self.debugging?
35
+ debug!
36
+ yield
37
+ ensure
38
+ self.debugging = original
39
+ end
40
+ self.debugging = false
41
+
42
+ def self.http_client
43
+ Faraday.new(headers: {user_agent: "JSON::JWK::Set::Fetcher #{VERSION}"}) do |faraday|
44
+ faraday.response :raise_error
45
+ faraday.response :follow_redirects
46
+ faraday.response :logger, JSON::JWK::Set::Fetcher.logger if debugging?
47
+ faraday.adapter Faraday.default_adapter
48
+ http_config.try(:call, faraday)
49
+ end
50
+ end
51
+ def self.http_config(&block)
52
+ @@http_config ||= block
53
+ end
54
+
55
+ def self.cache=(cache)
56
+ @@cache = cache
57
+ end
58
+ def self.cache
59
+ @@cache
60
+ end
61
+ self.cache = Cache.new
62
+
63
+ def self.fetch(jwks_uri, kid:, auto_detect: true, **options)
64
+ cache_key = [
65
+ 'json:jwk:set',
66
+ OpenSSL::Digest::MD5.hexdigest(jwks_uri),
67
+ kid
68
+ ].collect(&:to_s).join(':')
69
+
70
+ parsed_jwks = JSON.parse(
71
+ cache.fetch(cache_key, options) do
72
+ http_client.get(jwks_uri).body
73
+ end
74
+ )
75
+
76
+ unless parsed_jwks.is_a?(Hash) && parsed_jwks['keys'].is_a?(Array)
77
+ cache.delete(cache_key, options)
78
+ raise UnexpectedFormat
79
+ end
80
+
81
+ jwks = Set.new(parsed_jwks)
82
+ cache.delete(cache_key, options) if jwks[kid].blank?
83
+
84
+ if auto_detect
85
+ jwks[kid] or raise KidNotFound
86
+ else
87
+ jwks
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
data/lib/json/jwk/set.rb CHANGED
@@ -19,10 +19,16 @@ module JSON
19
19
  'application/jwk-set+json'
20
20
  end
21
21
 
22
+ def [](kid)
23
+ detect do |jwk|
24
+ jwk[:kid] && jwk[:kid] == kid
25
+ end
26
+ end
27
+
22
28
  def as_json(options = {})
23
29
  # NOTE: Array.new wrapper is requied to avoid CircularReferenceError
24
30
  {keys: Array.new(self)}
25
31
  end
26
32
  end
27
33
  end
28
- end
34
+ end