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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +92 -23
- data/CONTRIBUTING.md +7 -7
- data/README.md +125 -47
- data/lib/jwt/base64.rb +16 -2
- data/lib/jwt/claims_validator.rb +1 -1
- data/lib/jwt/configuration/container.rb +14 -3
- data/lib/jwt/decode.rb +41 -24
- data/lib/jwt/deprecations.rb +29 -0
- data/lib/jwt/encode.rb +23 -19
- data/lib/jwt/error.rb +1 -0
- data/lib/jwt/{algos → jwa}/ecdsa.rb +19 -7
- data/lib/jwt/jwa/eddsa.rb +42 -0
- data/lib/jwt/jwa/hmac.rb +75 -0
- data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
- data/lib/jwt/{algos → jwa}/none.rb +4 -2
- data/lib/jwt/jwa/ps.rb +30 -0
- data/lib/jwt/jwa/rsa.rb +25 -0
- data/lib/jwt/{algos → jwa}/unsupported.rb +1 -1
- data/lib/jwt/jwa/wrapper.rb +26 -0
- data/lib/jwt/jwa.rb +62 -0
- data/lib/jwt/jwk/ec.rb +168 -116
- data/lib/jwt/jwk/hmac.rb +64 -28
- data/lib/jwt/jwk/key_base.rb +33 -11
- data/lib/jwt/jwk/key_finder.rb +19 -35
- data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
- data/lib/jwt/jwk/rsa.rb +142 -77
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk.rb +14 -11
- data/lib/jwt/verify.rb +8 -4
- data/lib/jwt/version.rb +20 -3
- data/lib/jwt/x5c_key_finder.rb +0 -3
- data/lib/jwt.rb +1 -0
- data/ruby-jwt.gemspec +11 -4
- metadata +35 -27
- data/.codeclimate.yml +0 -8
- data/.github/workflows/coverage.yml +0 -27
- data/.github/workflows/test.yml +0 -67
- data/.gitignore +0 -13
- data/.reek.yml +0 -22
- data/.rspec +0 -2
- data/.rubocop.yml +0 -67
- data/.sourcelevel.yml +0 -17
- data/Appraisals +0 -13
- data/Gemfile +0 -7
- data/Rakefile +0 -16
- data/lib/jwt/algos/eddsa.rb +0 -35
- data/lib/jwt/algos/hmac.rb +0 -36
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -22
- data/lib/jwt/algos.rb +0 -44
- data/lib/jwt/security_utils.rb +0 -59
- 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
|
-
|
19
|
+
# For backwards compatibility when kid was a String
|
20
|
+
params = { kid: params } if params.is_a?(String)
|
16
21
|
|
17
|
-
|
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
|
-
|
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
|
-
|
30
|
+
def keypair
|
31
|
+
ec_key
|
23
32
|
end
|
24
33
|
|
25
34
|
def private?
|
26
|
-
|
35
|
+
ec_key.private_key?
|
27
36
|
end
|
28
37
|
|
29
|
-
def
|
30
|
-
|
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
|
40
|
-
|
42
|
+
def verify_key
|
43
|
+
ec_key
|
44
|
+
end
|
41
45
|
|
42
|
-
|
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
|
-
|
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(
|
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
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
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
|
-
|
12
|
+
def initialize(key, params = nil, options = {})
|
13
|
+
params ||= {}
|
10
14
|
|
11
|
-
|
12
|
-
|
15
|
+
# For backwards compatibility when kid was a String
|
16
|
+
params = { kid: params } if params.is_a?(String)
|
13
17
|
|
14
|
-
|
15
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
kty: KTY,
|
30
|
-
kid: kid
|
31
|
-
}
|
38
|
+
def verify_key
|
39
|
+
secret
|
40
|
+
end
|
32
41
|
|
33
|
-
|
42
|
+
def signing_key
|
43
|
+
secret
|
44
|
+
end
|
34
45
|
|
35
|
-
|
36
|
-
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
96
|
+
class << self
|
97
|
+
def import(jwk_data)
|
98
|
+
new(jwk_data)
|
63
99
|
end
|
64
100
|
end
|
65
101
|
end
|
data/lib/jwt/jwk/key_base.rb
CHANGED
@@ -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
|
-
|
15
|
-
options = { kid: options }
|
16
|
-
end
|
14
|
+
@parameters = params.transform_keys(&:to_sym) # Uniform interface
|
17
15
|
|
18
|
-
|
19
|
-
|
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
|
-
|
25
|
+
self[:kid]
|
24
26
|
end
|
25
27
|
|
26
|
-
|
28
|
+
def hash
|
29
|
+
self[:kid].hash
|
30
|
+
end
|
27
31
|
|
28
|
-
|
32
|
+
def [](key)
|
33
|
+
@parameters[key.to_sym]
|
34
|
+
end
|
29
35
|
|
30
|
-
def
|
31
|
-
|
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
|
data/lib/jwt/jwk/key_finder.rb
CHANGED
@@ -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
|
-
|
9
|
-
@
|
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'
|
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
|
-
|
26
|
+
jwk.verify_key
|
21
27
|
end
|
22
28
|
|
23
29
|
private
|
24
30
|
|
25
31
|
def resolve_key(kid)
|
26
|
-
|
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
|
-
|
39
|
-
|
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
|
-
|
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
|
-
|
58
|
-
@
|
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
|