jwt 2.4.1 → 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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -14
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +154 -37
  5. data/lib/jwt/base64.rb +33 -0
  6. data/lib/jwt/claims_validator.rb +1 -1
  7. data/lib/jwt/configuration/container.rb +32 -0
  8. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  9. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  10. data/lib/jwt/configuration.rb +15 -0
  11. data/lib/jwt/decode.rb +44 -29
  12. data/lib/jwt/deprecations.rb +29 -0
  13. data/lib/jwt/encode.rb +24 -20
  14. data/lib/jwt/error.rb +1 -0
  15. data/lib/jwt/{algos → jwa}/ecdsa.rb +19 -7
  16. data/lib/jwt/jwa/eddsa.rb +42 -0
  17. data/lib/jwt/jwa/hmac.rb +75 -0
  18. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  19. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  20. data/lib/jwt/{algos → jwa}/none.rb +4 -2
  21. data/lib/jwt/jwa/ps.rb +30 -0
  22. data/lib/jwt/jwa/rsa.rb +25 -0
  23. data/lib/jwt/{algos → jwa}/unsupported.rb +1 -1
  24. data/lib/jwt/jwa/wrapper.rb +26 -0
  25. data/lib/jwt/jwa.rb +62 -0
  26. data/lib/jwt/jwk/ec.rb +160 -63
  27. data/lib/jwt/jwk/hmac.rb +69 -24
  28. data/lib/jwt/jwk/key_base.rb +45 -7
  29. data/lib/jwt/jwk/key_finder.rb +19 -35
  30. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  31. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  32. data/lib/jwt/jwk/rsa.rb +141 -54
  33. data/lib/jwt/jwk/set.rb +80 -0
  34. data/lib/jwt/jwk/thumbprint.rb +26 -0
  35. data/lib/jwt/jwk.rb +14 -11
  36. data/lib/jwt/verify.rb +8 -4
  37. data/lib/jwt/version.rb +23 -1
  38. data/lib/jwt/x5c_key_finder.rb +1 -4
  39. data/lib/jwt.rb +6 -4
  40. data/ruby-jwt.gemspec +11 -4
  41. metadata +41 -27
  42. data/.codeclimate.yml +0 -8
  43. data/.github/workflows/coverage.yml +0 -27
  44. data/.github/workflows/test.yml +0 -66
  45. data/.gitignore +0 -13
  46. data/.reek.yml +0 -22
  47. data/.rspec +0 -2
  48. data/.rubocop.yml +0 -67
  49. data/.sourcelevel.yml +0 -17
  50. data/Appraisals +0 -13
  51. data/Gemfile +0 -7
  52. data/Rakefile +0 -16
  53. data/lib/jwt/algos/eddsa.rb +0 -33
  54. data/lib/jwt/algos/hmac.rb +0 -36
  55. data/lib/jwt/algos/ps.rb +0 -43
  56. data/lib/jwt/algos/rsa.rb +0 -22
  57. data/lib/jwt/algos.rb +0 -44
  58. data/lib/jwt/default_options.rb +0 -18
  59. data/lib/jwt/security_utils.rb +0 -59
  60. data/lib/jwt/signature.rb +0 -35
data/lib/jwt/jwa.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ begin
6
+ require 'rbnacl'
7
+ rescue LoadError
8
+ raise if defined?(RbNaCl)
9
+ end
10
+
11
+ require_relative 'jwa/hmac'
12
+ require_relative 'jwa/eddsa'
13
+ require_relative 'jwa/ecdsa'
14
+ require_relative 'jwa/rsa'
15
+ require_relative 'jwa/ps'
16
+ require_relative 'jwa/none'
17
+ require_relative 'jwa/unsupported'
18
+ require_relative 'jwa/wrapper'
19
+
20
+ module JWT
21
+ module JWA
22
+ ALGOS = [Hmac, Ecdsa, Rsa, Eddsa, Ps, None, Unsupported].tap do |l|
23
+ if ::JWT.rbnacl_6_or_greater?
24
+ require_relative 'jwa/hmac_rbnacl'
25
+ l << Algos::HmacRbNaCl
26
+ elsif ::JWT.rbnacl?
27
+ require_relative 'jwa/hmac_rbnacl_fixed'
28
+ l << Algos::HmacRbNaClFixed
29
+ end
30
+ end.freeze
31
+
32
+ class << self
33
+ def find(algorithm)
34
+ indexed[algorithm&.downcase]
35
+ end
36
+
37
+ def create(algorithm)
38
+ return algorithm if JWA.implementation?(algorithm)
39
+
40
+ Wrapper.new(*find(algorithm))
41
+ end
42
+
43
+ def implementation?(algorithm)
44
+ (algorithm.respond_to?(:valid_alg?) && algorithm.respond_to?(:verify)) ||
45
+ (algorithm.respond_to?(:alg) && algorithm.respond_to?(:sign))
46
+ end
47
+
48
+ private
49
+
50
+ def indexed
51
+ @indexed ||= begin
52
+ fallback = [nil, Unsupported]
53
+ ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
54
+ cls.const_get(:SUPPORTED).each do |alg|
55
+ hash[alg.downcase] = [alg, cls]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/jwt/jwk/ec.rb CHANGED
@@ -4,55 +4,100 @@ require 'forwardable'
4
4
 
5
5
  module JWT
6
6
  module JWK
7
- class EC < KeyBase
8
- extend Forwardable
9
- def_delegators :@keypair, :public_key
10
-
7
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
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 ||= {}
18
+
19
+ # For backwards compatibility when kid was a String
20
+ params = { kid: params } if params.is_a?(String)
21
+
22
+ key_params = extract_key_params(key)
23
+
24
+ params = params.transform_keys(&:to_sym)
25
+ check_jwk_params!(key_params, params)
14
26
 
15
- def initialize(keypair, kid = nil)
16
- raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
27
+ super(options, key_params.merge(params))
28
+ end
17
29
 
18
- kid ||= generate_kid(keypair)
19
- super(keypair, kid)
30
+ def keypair
31
+ ec_key
20
32
  end
21
33
 
22
34
  def private?
23
- @keypair.private_key?
35
+ ec_key.private_key?
24
36
  end
25
37
 
26
- def export(options = {})
27
- crv, x_octets, y_octets = keypair_components(keypair)
28
- exported_hash = {
29
- kty: KTY,
30
- crv: crv,
31
- x: encode_octets(x_octets),
32
- y: encode_octets(y_octets),
33
- kid: kid
34
- }
35
- return exported_hash unless private? && options[:include_private] == true
38
+ def signing_key
39
+ ec_key
40
+ end
36
41
 
37
- append_private_parts(exported_hash)
42
+ def verify_key
43
+ ec_key
38
44
  end
39
45
 
40
- private
46
+ def public_key
47
+ ec_key
48
+ end
41
49
 
42
- def append_private_parts(the_hash)
43
- octets = keypair.private_key.to_bn.to_s(BINARY)
44
- the_hash.merge(
45
- d: encode_octets(octets)
46
- )
50
+ def members
51
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
47
52
  end
48
53
 
49
- def generate_kid(ec_keypair)
50
- _crv, x_octets, y_octets = keypair_components(ec_keypair)
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
58
+ end
59
+
60
+ def key_digest
61
+ _crv, x_octets, y_octets = keypair_components(ec_key)
51
62
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
52
63
  OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
53
64
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
54
65
  end
55
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
+
75
+ private
76
+
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]
99
+ end
100
+
56
101
  def keypair_components(ec_keypair)
57
102
  encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
58
103
  case ec_keypair.group.curve_name
@@ -75,47 +120,65 @@ module JWT
75
120
  end
76
121
 
77
122
  def encode_octets(octets)
78
- Base64.urlsafe_encode64(octets, padding: false)
123
+ return unless octets
124
+
125
+ ::JWT::Base64.url_encode(octets)
79
126
  end
80
127
 
81
128
  def encode_open_ssl_bn(key_part)
82
- Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
129
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
83
130
  end
84
131
 
85
- class << self
86
- def import(jwk_data)
87
- # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
88
- # 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
89
143
 
90
- jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
91
- raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
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)
92
149
 
93
- new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
94
- end
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
+ )
95
154
 
96
- def to_openssl_curve(crv)
97
- # The JWK specs and OpenSSL use different names for the same curves.
98
- # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
99
- # pointers on different names for common curves.
100
- case crv
101
- when 'P-256' then 'prime256v1'
102
- when 'P-384' then 'secp384r1'
103
- when 'P-521' then 'secp521r1'
104
- when 'P-256K' then 'secp256k1'
105
- else raise JWT::JWKError, 'Invalid curve provided'
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
+ ])
106
175
  end
107
- end
108
-
109
- private
110
176
 
111
- def jwk_attrs(jwk_data, attrs)
112
- attrs.map do |attr|
113
- jwk_data[attr] || jwk_data[attr.to_s]
114
- end
177
+ OpenSSL::PKey::EC.new(sequence.to_der)
115
178
  end
116
-
117
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
118
- curve = to_openssl_curve(jwk_crv)
179
+ else
180
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
181
+ curve = EC.to_openssl_curve(jwk_crv)
119
182
 
120
183
  x_octets = decode_octets(jwk_x)
121
184
  y_octets = decode_octets(jwk_y)
@@ -140,13 +203,47 @@ module JWT
140
203
 
141
204
  key
142
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
143
230
 
144
- def decode_octets(jwk_data)
145
- Base64.urlsafe_decode64(jwk_data)
231
+ class << self
232
+ def import(jwk_data)
233
+ new(jwk_data)
146
234
  end
147
235
 
148
- def decode_open_ssl_bn(jwk_data)
149
- OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
236
+ def to_openssl_curve(crv)
237
+ # The JWK specs and OpenSSL use different names for the same curves.
238
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
239
+ # pointers on different names for common curves.
240
+ case crv
241
+ when 'P-256' then 'prime256v1'
242
+ when 'P-384' then 'secp384r1'
243
+ when 'P-521' then 'secp521r1'
244
+ when 'P-256K' then 'secp256k1'
245
+ else raise JWT::JWKError, 'Invalid curve provided'
246
+ end
150
247
  end
151
248
  end
152
249
  end
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -3,14 +3,28 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class HMAC < KeyBase
6
- KTY = 'oct'
7
- KTYS = [KTY, String].freeze
6
+ KTY = 'oct'
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
- def initialize(keypair, kid = nil)
10
- raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
12
+ def initialize(key, params = nil, options = {})
13
+ params ||= {}
11
14
 
12
- super
13
- @kid = kid || generate_kid
15
+ # For backwards compatibility when kid was a String
16
+ params = { kid: params } if params.is_a?(String)
17
+
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
14
28
  end
15
29
 
16
30
  def private?
@@ -21,36 +35,67 @@ module JWT
21
35
  nil
22
36
  end
23
37
 
38
+ def verify_key
39
+ secret
40
+ end
41
+
42
+ def signing_key
43
+ secret
44
+ end
45
+
24
46
  # See https://tools.ietf.org/html/rfc7517#appendix-A.3
25
47
  def export(options = {})
26
- exported_hash = {
27
- kty: KTY,
28
- kid: kid
29
- }
48
+ exported = parameters.clone
49
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
50
+ exported
51
+ end
52
+
53
+ def members
54
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
55
+ end
56
+
57
+ def key_digest
58
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
59
+ OpenSSL::ASN1::UTF8String.new(KTY)])
60
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
61
+ end
30
62
 
31
- return exported_hash unless private? && options[:include_private] == true
63
+ def []=(key, value)
64
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
65
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
66
+ end
32
67
 
33
- exported_hash.merge(
34
- k: keypair
35
- )
68
+ super(key, value)
36
69
  end
37
70
 
38
71
  private
39
72
 
40
- def generate_kid
41
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
42
- OpenSSL::ASN1::UTF8String.new(KTY)])
43
- OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
73
+ def secret
74
+ self[:k]
44
75
  end
45
76
 
46
- class << self
47
- def import(jwk_data)
48
- jwk_k = jwk_data[:k] || jwk_data['k']
49
- jwk_kid = jwk_data[:kid] || jwk_data['kid']
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
50
89
 
51
- raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
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
52
95
 
53
- new(jwk_k, jwk_kid)
96
+ class << self
97
+ def import(jwk_data)
98
+ new(jwk_data)
54
99
  end
55
100
  end
56
101
  end
@@ -3,17 +3,55 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class KeyBase
6
- attr_reader :keypair, :kid
7
-
8
- def initialize(keypair, kid = nil)
9
- @keypair = keypair
10
- @kid = kid
11
- end
12
-
13
6
  def self.inherited(klass)
14
7
  super
15
8
  ::JWT::JWK.classes << klass
16
9
  end
10
+
11
+ def initialize(options, params = {})
12
+ options ||= {}
13
+
14
+ @parameters = params.transform_keys(&:to_sym) # Uniform interface
15
+
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
22
+ end
23
+
24
+ def kid
25
+ self[:kid]
26
+ end
27
+
28
+ def hash
29
+ self[:kid].hash
30
+ end
31
+
32
+ def [](key)
33
+ @parameters[key.to_sym]
34
+ end
35
+
36
+ def []=(key, value)
37
+ @parameters[key.to_sym] = value
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
17
55
  end
18
56
  end
19
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)
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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KidAsKeyDigest
6
+ def initialize(jwk)
7
+ @jwk = jwk
8
+ end
9
+
10
+ def generate
11
+ @jwk.key_digest
12
+ end
13
+ end
14
+ end
15
+ end