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