jwt 2.5.0 → 2.8.1

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -23
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +125 -47
  5. data/lib/jwt/base64.rb +16 -2
  6. data/lib/jwt/claims_validator.rb +1 -1
  7. data/lib/jwt/configuration/container.rb +14 -3
  8. data/lib/jwt/decode.rb +41 -24
  9. data/lib/jwt/deprecations.rb +29 -0
  10. data/lib/jwt/encode.rb +23 -19
  11. data/lib/jwt/error.rb +1 -0
  12. data/lib/jwt/{algos → jwa}/ecdsa.rb +19 -7
  13. data/lib/jwt/jwa/eddsa.rb +42 -0
  14. data/lib/jwt/jwa/hmac.rb +75 -0
  15. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  16. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  17. data/lib/jwt/{algos → jwa}/none.rb +4 -2
  18. data/lib/jwt/jwa/ps.rb +30 -0
  19. data/lib/jwt/jwa/rsa.rb +25 -0
  20. data/lib/jwt/{algos → jwa}/unsupported.rb +1 -1
  21. data/lib/jwt/jwa/wrapper.rb +26 -0
  22. data/lib/jwt/jwa.rb +62 -0
  23. data/lib/jwt/jwk/ec.rb +168 -116
  24. data/lib/jwt/jwk/hmac.rb +64 -28
  25. data/lib/jwt/jwk/key_base.rb +33 -11
  26. data/lib/jwt/jwk/key_finder.rb +19 -35
  27. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  28. data/lib/jwt/jwk/rsa.rb +142 -77
  29. data/lib/jwt/jwk/set.rb +80 -0
  30. data/lib/jwt/jwk.rb +14 -11
  31. data/lib/jwt/verify.rb +8 -4
  32. data/lib/jwt/version.rb +20 -3
  33. data/lib/jwt/x5c_key_finder.rb +0 -3
  34. data/lib/jwt.rb +1 -0
  35. data/ruby-jwt.gemspec +11 -4
  36. metadata +35 -27
  37. data/.codeclimate.yml +0 -8
  38. data/.github/workflows/coverage.yml +0 -27
  39. data/.github/workflows/test.yml +0 -67
  40. data/.gitignore +0 -13
  41. data/.reek.yml +0 -22
  42. data/.rspec +0 -2
  43. data/.rubocop.yml +0 -67
  44. data/.sourcelevel.yml +0 -17
  45. data/Appraisals +0 -13
  46. data/Gemfile +0 -7
  47. data/Rakefile +0 -16
  48. data/lib/jwt/algos/eddsa.rb +0 -35
  49. data/lib/jwt/algos/hmac.rb +0 -36
  50. data/lib/jwt/algos/ps.rb +0 -43
  51. data/lib/jwt/algos/rsa.rb +0 -22
  52. data/lib/jwt/algos.rb +0 -44
  53. data/lib/jwt/security_utils.rb +0 -59
  54. data/lib/jwt/signature.rb +0 -35
data/lib/jwt/jwk/ec.rb CHANGED
@@ -5,59 +5,97 @@ require 'forwardable'
5
5
  module JWT
6
6
  module JWK
7
7
  class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
- extend Forwardable
9
- def_delegators :keypair, :public_key
10
-
11
8
  KTY = 'EC'
12
- KTYS = [KTY, OpenSSL::PKey::EC].freeze
9
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
13
10
  BINARY = 2
11
+ EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
12
+ EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
13
+ EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
14
+ ZERO_BYTE = "\0".b.freeze
15
+
16
+ def initialize(key, params = nil, options = {})
17
+ params ||= {}
14
18
 
15
- attr_reader :keypair
19
+ # For backwards compatibility when kid was a String
20
+ params = { kid: params } if params.is_a?(String)
16
21
 
17
- def initialize(keypair, options = {})
18
- raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
22
+ key_params = extract_key_params(key)
19
23
 
20
- @keypair = keypair
24
+ params = params.transform_keys(&:to_sym)
25
+ check_jwk_params!(key_params, params)
26
+
27
+ super(options, key_params.merge(params))
28
+ end
21
29
 
22
- super(options)
30
+ def keypair
31
+ ec_key
23
32
  end
24
33
 
25
34
  def private?
26
- @keypair.private_key?
35
+ ec_key.private_key?
27
36
  end
28
37
 
29
- def members
30
- crv, x_octets, y_octets = keypair_components(keypair)
31
- {
32
- kty: KTY,
33
- crv: crv,
34
- x: encode_octets(x_octets),
35
- y: encode_octets(y_octets)
36
- }
38
+ def signing_key
39
+ ec_key
37
40
  end
38
41
 
39
- def export(options = {})
40
- exported_hash = members.merge(kid: kid)
42
+ def verify_key
43
+ ec_key
44
+ end
41
45
 
42
- return exported_hash unless private? && options[:include_private] == true
46
+ def public_key
47
+ ec_key
48
+ end
49
+
50
+ def members
51
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
52
+ end
43
53
 
44
- append_private_parts(exported_hash)
54
+ def export(options = {})
55
+ exported = parameters.clone
56
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
57
+ exported
45
58
  end
46
59
 
47
60
  def key_digest
48
- _crv, x_octets, y_octets = keypair_components(keypair)
61
+ _crv, x_octets, y_octets = keypair_components(ec_key)
49
62
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
50
63
  OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
51
64
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
52
65
  end
53
66
 
67
+ def []=(key, value)
68
+ if EC_KEY_ELEMENTS.include?(key.to_sym)
69
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
70
+ end
71
+
72
+ super(key, value)
73
+ end
74
+
54
75
  private
55
76
 
56
- def append_private_parts(the_hash)
57
- octets = keypair.private_key.to_bn.to_s(BINARY)
58
- the_hash.merge(
59
- d: encode_octets(octets)
60
- )
77
+ def ec_key
78
+ @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
79
+ end
80
+
81
+ def extract_key_params(key)
82
+ case key
83
+ when JWT::JWK::EC
84
+ key.export(include_private: true)
85
+ when OpenSSL::PKey::EC # Accept OpenSSL key as input
86
+ @ec_key = key # Preserve the object to avoid recreation
87
+ parse_ec_key(key)
88
+ when Hash
89
+ key.transform_keys(&:to_sym)
90
+ else
91
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
92
+ end
93
+ end
94
+
95
+ def check_jwk_params!(key_params, params)
96
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
97
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
98
+ raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
61
99
  end
62
100
 
63
101
  def keypair_components(ec_keypair)
@@ -82,6 +120,8 @@ module JWT
82
120
  end
83
121
 
84
122
  def encode_octets(octets)
123
+ return unless octets
124
+
85
125
  ::JWT::Base64.url_encode(octets)
86
126
  end
87
127
 
@@ -89,15 +129,108 @@ module JWT
89
129
  ::JWT::Base64.url_encode(key_part.to_s(BINARY))
90
130
  end
91
131
 
92
- class << self
93
- def import(jwk_data)
94
- # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
95
- # explanation of the relevant parameters.
132
+ def parse_ec_key(key)
133
+ crv, x_octets, y_octets = keypair_components(key)
134
+ octets = key.private_key&.to_bn&.to_s(BINARY)
135
+ {
136
+ kty: KTY,
137
+ crv: crv,
138
+ x: encode_octets(x_octets),
139
+ y: encode_octets(y_octets),
140
+ d: encode_octets(octets)
141
+ }.compact
142
+ end
143
+
144
+ if ::JWT.openssl_3?
145
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
146
+ curve = EC.to_openssl_curve(jwk_crv)
147
+ x_octets = decode_octets(jwk_x)
148
+ y_octets = decode_octets(jwk_y)
149
+
150
+ point = OpenSSL::PKey::EC::Point.new(
151
+ OpenSSL::PKey::EC::Group.new(curve),
152
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
153
+ )
154
+
155
+ sequence = if jwk_d
156
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
157
+ # ECPrivateKey ::= SEQUENCE {
158
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
159
+ # privateKey OCTET STRING,
160
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
161
+ # publicKey [1] BIT STRING OPTIONAL
162
+ # }
163
+
164
+ OpenSSL::ASN1::Sequence([
165
+ OpenSSL::ASN1::Integer(1),
166
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
167
+ OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
168
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
169
+ ])
170
+ else
171
+ OpenSSL::ASN1::Sequence([
172
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
173
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
174
+ ])
175
+ end
96
176
 
97
- jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
98
- raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
177
+ OpenSSL::PKey::EC.new(sequence.to_der)
178
+ end
179
+ else
180
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
181
+ curve = EC.to_openssl_curve(jwk_crv)
182
+
183
+ x_octets = decode_octets(jwk_x)
184
+ y_octets = decode_octets(jwk_y)
185
+
186
+ key = OpenSSL::PKey::EC.new(curve)
187
+
188
+ # The details of the `Point` instantiation are covered in:
189
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
190
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
191
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
192
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
193
+ # Section 2.3.3 of the last of these references specifies that the
194
+ # encoding of an uncompressed point consists of the byte `0x04` followed
195
+ # by the x value then the y value.
196
+ point = OpenSSL::PKey::EC::Point.new(
197
+ OpenSSL::PKey::EC::Group.new(curve),
198
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
199
+ )
200
+
201
+ key.public_key = point
202
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
203
+
204
+ key
205
+ end
206
+ end
207
+
208
+ def decode_octets(base64_encoded_coordinate)
209
+ bytes = ::JWT::Base64.url_decode(base64_encoded_coordinate)
210
+ # Some base64 encoders on some platform omit a single 0-byte at
211
+ # the start of either Y or X coordinate of the elliptic curve point.
212
+ # This leads to an encoding error when data is passed to OpenSSL BN.
213
+ # It is know to have happend to exported JWKs on a Java application and
214
+ # on a Flutter/Dart application (both iOS and Android). All that is
215
+ # needed to fix the problem is adding a leading 0-byte. We know the
216
+ # required byte is 0 because with any other byte the point is no longer
217
+ # on the curve - and OpenSSL will actually communicate this via another
218
+ # exception. The indication of a stripped byte will be the fact that the
219
+ # coordinates - once decoded into bytes - should always be an even
220
+ # bytesize. For example, with a P-521 curve, both x and y must be 66 bytes.
221
+ # With a P-256 curve, both x and y must be 32 and so on. The simplest way
222
+ # to check for this truncation is thus to check whether the number of bytes
223
+ # is odd, and restore the leading 0-byte if it is.
224
+ if bytes.bytesize.odd?
225
+ ZERO_BYTE + bytes
226
+ else
227
+ bytes
228
+ end
229
+ end
99
230
 
100
- new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid)
231
+ class << self
232
+ def import(jwk_data)
233
+ new(jwk_data)
101
234
  end
102
235
 
103
236
  def to_openssl_curve(crv)
@@ -112,87 +245,6 @@ module JWT
112
245
  else raise JWT::JWKError, 'Invalid curve provided'
113
246
  end
114
247
  end
115
-
116
- private
117
-
118
- def jwk_attrs(jwk_data, attrs)
119
- attrs.map do |attr|
120
- jwk_data[attr] || jwk_data[attr.to_s]
121
- end
122
- end
123
-
124
- if ::JWT.openssl_3?
125
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
126
- curve = to_openssl_curve(jwk_crv)
127
-
128
- x_octets = decode_octets(jwk_x)
129
- y_octets = decode_octets(jwk_y)
130
-
131
- point = OpenSSL::PKey::EC::Point.new(
132
- OpenSSL::PKey::EC::Group.new(curve),
133
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
134
- )
135
-
136
- sequence = if jwk_d
137
- # https://datatracker.ietf.org/doc/html/rfc5915.html
138
- # ECPrivateKey ::= SEQUENCE {
139
- # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
140
- # privateKey OCTET STRING,
141
- # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
142
- # publicKey [1] BIT STRING OPTIONAL
143
- # }
144
-
145
- OpenSSL::ASN1::Sequence([
146
- OpenSSL::ASN1::Integer(1),
147
- OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
148
- OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
149
- OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
150
- ])
151
- else
152
- OpenSSL::ASN1::Sequence([
153
- OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
154
- OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
155
- ])
156
- end
157
-
158
- OpenSSL::PKey::EC.new(sequence.to_der)
159
- end
160
- else
161
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
162
- curve = to_openssl_curve(jwk_crv)
163
-
164
- x_octets = decode_octets(jwk_x)
165
- y_octets = decode_octets(jwk_y)
166
-
167
- key = OpenSSL::PKey::EC.new(curve)
168
-
169
- # The details of the `Point` instantiation are covered in:
170
- # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
171
- # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
172
- # - https://tools.ietf.org/html/rfc5480#section-2.2
173
- # - https://www.secg.org/SEC1-Ver-1.0.pdf
174
- # Section 2.3.3 of the last of these references specifies that the
175
- # encoding of an uncompressed point consists of the byte `0x04` followed
176
- # by the x value then the y value.
177
- point = OpenSSL::PKey::EC::Point.new(
178
- OpenSSL::PKey::EC::Group.new(curve),
179
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
180
- )
181
-
182
- key.public_key = point
183
- key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
184
-
185
- key
186
- end
187
- end
188
-
189
- def decode_octets(jwk_data)
190
- ::JWT::Base64.url_decode(jwk_data)
191
- end
192
-
193
- def decode_open_ssl_bn(jwk_data)
194
- OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
195
- end
196
248
  end
197
249
  end
198
250
  end
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -4,15 +4,27 @@ module JWT
4
4
  module JWK
5
5
  class HMAC < KeyBase
6
6
  KTY = 'oct'
7
- KTYS = [KTY, String].freeze
7
+ KTYS = [KTY, String, JWT::JWK::HMAC].freeze
8
+ HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze
9
+ HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze
10
+ HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze
8
11
 
9
- attr_reader :signing_key
12
+ def initialize(key, params = nil, options = {})
13
+ params ||= {}
10
14
 
11
- def initialize(signing_key, options = {})
12
- raise ArgumentError, 'signing_key must be of type String' unless signing_key.is_a?(String)
15
+ # For backwards compatibility when kid was a String
16
+ params = { kid: params } if params.is_a?(String)
13
17
 
14
- @signing_key = signing_key
15
- super(options)
18
+ key_params = extract_key_params(key)
19
+
20
+ params = params.transform_keys(&:to_sym)
21
+ check_jwk(key_params, params)
22
+
23
+ super(options, key_params.merge(params))
24
+ end
25
+
26
+ def keypair
27
+ secret
16
28
  end
17
29
 
18
30
  def private?
@@ -23,43 +35,67 @@ module JWT
23
35
  nil
24
36
  end
25
37
 
26
- # See https://tools.ietf.org/html/rfc7517#appendix-A.3
27
- def export(options = {})
28
- exported_hash = {
29
- kty: KTY,
30
- kid: kid
31
- }
38
+ def verify_key
39
+ secret
40
+ end
32
41
 
33
- return exported_hash unless private? && options[:include_private] == true
42
+ def signing_key
43
+ secret
44
+ end
34
45
 
35
- exported_hash.merge(
36
- k: signing_key
37
- )
46
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
47
+ def export(options = {})
48
+ exported = parameters.clone
49
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
50
+ exported
38
51
  end
39
52
 
40
53
  def members
41
- {
42
- kty: KTY,
43
- k: signing_key
44
- }
54
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
45
55
  end
46
56
 
47
- alias keypair signing_key # for backwards compatibility
48
-
49
57
  def key_digest
50
58
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
51
59
  OpenSSL::ASN1::UTF8String.new(KTY)])
52
60
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
53
61
  end
54
62
 
55
- class << self
56
- def import(jwk_data)
57
- jwk_k = jwk_data[:k] || jwk_data['k']
58
- jwk_kid = jwk_data[:kid] || jwk_data['kid']
63
+ def []=(key, value)
64
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
65
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
66
+ end
67
+
68
+ super(key, value)
69
+ end
70
+
71
+ private
72
+
73
+ def secret
74
+ self[:k]
75
+ end
59
76
 
60
- raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
77
+ def extract_key_params(key)
78
+ case key
79
+ when JWT::JWK::HMAC
80
+ key.export(include_private: true)
81
+ when String # Accept String key as input
82
+ { kty: KTY, k: key }
83
+ when Hash
84
+ key.transform_keys(&:to_sym)
85
+ else
86
+ raise ArgumentError, 'key must be of type String or Hash with key parameters'
87
+ end
88
+ end
89
+
90
+ def check_jwk(keypair, params)
91
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
92
+ raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
93
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
94
+ end
61
95
 
62
- new(jwk_k, kid: jwk_kid)
96
+ class << self
97
+ def import(jwk_data)
98
+ new(jwk_data)
63
99
  end
64
100
  end
65
101
  end
@@ -8,28 +8,50 @@ module JWT
8
8
  ::JWT::JWK.classes << klass
9
9
  end
10
10
 
11
- def initialize(options)
11
+ def initialize(options, params = {})
12
12
  options ||= {}
13
13
 
14
- if options.is_a?(String) # For backwards compatibility when kid was a String
15
- options = { kid: options }
16
- end
14
+ @parameters = params.transform_keys(&:to_sym) # Uniform interface
17
15
 
18
- @kid = options[:kid]
19
- @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
16
+ # For backwards compatibility, kid_generator may be specified in the parameters
17
+ options[:kid_generator] ||= @parameters.delete(:kid_generator)
18
+
19
+ # Make sure the key has a kid
20
+ kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
21
+ self[:kid] ||= kid_generator.new(self).generate
20
22
  end
21
23
 
22
24
  def kid
23
- @kid ||= generate_kid
25
+ self[:kid]
24
26
  end
25
27
 
26
- private
28
+ def hash
29
+ self[:kid].hash
30
+ end
27
31
 
28
- attr_reader :kid_generator
32
+ def [](key)
33
+ @parameters[key.to_sym]
34
+ end
29
35
 
30
- def generate_kid
31
- kid_generator.new(self).generate
36
+ def []=(key, value)
37
+ @parameters[key.to_sym] = value
32
38
  end
39
+
40
+ def ==(other)
41
+ other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid]
42
+ end
43
+
44
+ alias eql? ==
45
+
46
+ def <=>(other)
47
+ return nil unless other.is_a?(::JWT::JWK::KeyBase)
48
+
49
+ self[:kid] <=> other[:kid]
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :parameters
33
55
  end
34
56
  end
35
57
  end
@@ -4,58 +4,42 @@ module JWT
4
4
  module JWK
5
5
  class KeyFinder
6
6
  def initialize(options)
7
+ @allow_nil_kid = options[:allow_nil_kid]
7
8
  jwks_or_loader = options[:jwks]
8
- @jwks = jwks_or_loader if jwks_or_loader.is_a?(Hash)
9
- @jwk_loader = jwks_or_loader if jwks_or_loader.respond_to?(:call)
9
+
10
+ @jwks_loader = if jwks_or_loader.respond_to?(:call)
11
+ jwks_or_loader
12
+ else
13
+ ->(_options) { jwks_or_loader }
14
+ end
10
15
  end
11
16
 
12
17
  def key_for(kid)
13
- raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
18
+ raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
19
+ raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
14
20
 
15
21
  jwk = resolve_key(kid)
16
22
 
17
- raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
23
+ raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
18
24
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
19
25
 
20
- ::JWT::JWK.import(jwk).keypair
26
+ jwk.verify_key
21
27
  end
22
28
 
23
29
  private
24
30
 
25
31
  def resolve_key(kid)
26
- jwk = find_key(kid)
27
-
28
- return jwk if jwk
29
-
30
- if reloadable?
31
- load_keys(invalidate: true, kid_not_found: true, kid: kid) # invalidate for backwards compatibility
32
- return find_key(kid)
33
- end
34
-
35
- nil
36
- end
32
+ key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
37
33
 
38
- def jwks
39
- return @jwks if @jwks
34
+ # First try without invalidation to facilitate application caching
35
+ @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
36
+ jwk = @jwks.find { |key| key_matcher.call(key) }
40
37
 
41
- load_keys
42
- @jwks
43
- end
44
-
45
- def load_keys(opts = {})
46
- @jwks = @jwk_loader.call(opts)
47
- end
48
-
49
- def jwks_keys
50
- Array(jwks[:keys] || jwks['keys'])
51
- end
52
-
53
- def find_key(kid)
54
- jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
55
- end
38
+ return jwk if jwk
56
39
 
57
- def reloadable?
58
- @jwk_loader
40
+ # Second try, invalidate for backwards compatibility
41
+ @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
42
+ @jwks.find { |key| key_matcher.call(key) }
59
43
  end
60
44
  end
61
45
  end