jwt 2.2.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.
- checksums.yaml +4 -4
- data/AUTHORS +79 -44
- data/CHANGELOG.md +305 -20
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +268 -40
- data/lib/jwt/base64.rb +16 -2
- data/lib/jwt/claims_validator.rb +13 -9
- data/lib/jwt/configuration/container.rb +32 -0
- data/lib/jwt/configuration/decode_configuration.rb +46 -0
- data/lib/jwt/configuration/jwk_configuration.rb +27 -0
- data/lib/jwt/configuration.rb +15 -0
- data/lib/jwt/decode.rb +80 -18
- data/lib/jwt/deprecations.rb +29 -0
- data/lib/jwt/encode.rb +24 -19
- data/lib/jwt/error.rb +17 -14
- data/lib/jwt/jwa/ecdsa.rb +76 -0
- 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/jwa/none.rb +19 -0
- data/lib/jwt/jwa/ps.rb +30 -0
- data/lib/jwt/jwa/rsa.rb +25 -0
- data/lib/jwt/{algos → jwa}/unsupported.rb +8 -5
- data/lib/jwt/jwa/wrapper.rb +26 -0
- data/lib/jwt/jwa.rb +62 -0
- data/lib/jwt/jwk/ec.rb +251 -0
- data/lib/jwt/jwk/hmac.rb +103 -0
- data/lib/jwt/jwk/key_base.rb +57 -0
- data/lib/jwt/jwk/key_finder.rb +19 -30
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
- data/lib/jwt/jwk/rsa.rb +181 -25
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +39 -15
- data/lib/jwt/verify.rb +25 -6
- data/lib/jwt/version.rb +24 -3
- data/lib/jwt/x5c_key_finder.rb +52 -0
- data/lib/jwt.rb +6 -4
- data/ruby-jwt.gemspec +18 -10
- metadata +45 -76
- data/.codeclimate.yml +0 -20
- data/.ebert.yml +0 -18
- data/.gitignore +0 -11
- data/.rspec +0 -1
- data/.rubocop.yml +0 -98
- data/.travis.yml +0 -20
- data/Appraisals +0 -14
- data/Gemfile +0 -3
- data/Rakefile +0 -11
- data/lib/jwt/algos/ecdsa.rb +0 -35
- data/lib/jwt/algos/eddsa.rb +0 -23
- data/lib/jwt/algos/hmac.rb +0 -33
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -19
- data/lib/jwt/default_options.rb +0 -15
- data/lib/jwt/security_utils.rb +0 -57
- data/lib/jwt/signature.rb +0 -52
data/lib/jwt/jwk/key_finder.rb
CHANGED
@@ -4,53 +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
|
|
23
|
+
raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
|
17
24
|
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
|
18
25
|
|
19
|
-
|
26
|
+
jwk.verify_key
|
20
27
|
end
|
21
28
|
|
22
29
|
private
|
23
30
|
|
24
31
|
def resolve_key(kid)
|
25
|
-
|
26
|
-
|
27
|
-
return jwk if jwk
|
28
|
-
|
29
|
-
if reloadable?
|
30
|
-
load_keys(invalidate: true)
|
31
|
-
return find_key(kid)
|
32
|
-
end
|
33
|
-
|
34
|
-
nil
|
35
|
-
end
|
36
|
-
|
37
|
-
def jwks
|
38
|
-
return @jwks if @jwks
|
32
|
+
key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
|
39
33
|
|
40
|
-
|
41
|
-
@jwks
|
42
|
-
|
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) }
|
43
37
|
|
44
|
-
|
45
|
-
@jwks = @jwk_loader.call(opts)
|
46
|
-
end
|
47
|
-
|
48
|
-
def find_key(kid)
|
49
|
-
Array(jwks[:keys]).find { |key| key[:kid] == kid }
|
50
|
-
end
|
38
|
+
return jwk if jwk
|
51
39
|
|
52
|
-
|
53
|
-
@
|
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) }
|
54
43
|
end
|
55
44
|
end
|
56
45
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWK
|
5
|
+
class OKPRbNaCl < KeyBase
|
6
|
+
KTY = 'OKP'
|
7
|
+
KTYS = [KTY, JWT::JWK::OKPRbNaCl, RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey].freeze
|
8
|
+
OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze
|
9
|
+
OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze
|
10
|
+
|
11
|
+
def initialize(key, params = nil, options = {})
|
12
|
+
params ||= {}
|
13
|
+
|
14
|
+
# For backwards compatibility when kid was a String
|
15
|
+
params = { kid: params } if params.is_a?(String)
|
16
|
+
|
17
|
+
key_params = extract_key_params(key)
|
18
|
+
|
19
|
+
params = params.transform_keys(&:to_sym)
|
20
|
+
check_jwk_params!(key_params, params)
|
21
|
+
super(options, key_params.merge(params))
|
22
|
+
end
|
23
|
+
|
24
|
+
def verify_key
|
25
|
+
return @verify_key if defined?(@verify_key)
|
26
|
+
|
27
|
+
@verify_key = verify_key_from_parameters
|
28
|
+
end
|
29
|
+
|
30
|
+
def signing_key
|
31
|
+
return @signing_key if defined?(@signing_key)
|
32
|
+
|
33
|
+
@signing_key = signing_key_from_parameters
|
34
|
+
end
|
35
|
+
|
36
|
+
def key_digest
|
37
|
+
Thumbprint.new(self).to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def private?
|
41
|
+
!signing_key.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
def members
|
45
|
+
OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
46
|
+
end
|
47
|
+
|
48
|
+
def export(options = {})
|
49
|
+
exported = parameters.clone
|
50
|
+
exported.reject! { |k, _| OKP_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
|
51
|
+
exported
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def extract_key_params(key)
|
57
|
+
case key
|
58
|
+
when JWT::JWK::KeyBase
|
59
|
+
key.export(include_private: true)
|
60
|
+
when RbNaCl::Signatures::Ed25519::SigningKey
|
61
|
+
@signing_key = key
|
62
|
+
@verify_key = key.verify_key
|
63
|
+
parse_okp_key_params(@verify_key, @signing_key)
|
64
|
+
when RbNaCl::Signatures::Ed25519::VerifyKey
|
65
|
+
@signing_key = nil
|
66
|
+
@verify_key = key
|
67
|
+
parse_okp_key_params(@verify_key)
|
68
|
+
when Hash
|
69
|
+
key.transform_keys(&:to_sym)
|
70
|
+
else
|
71
|
+
raise ArgumentError, 'key must be of type RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey or Hash with key parameters'
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def check_jwk_params!(key_params, _given_params)
|
76
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse_okp_key_params(verify_key, signing_key = nil)
|
80
|
+
params = {
|
81
|
+
kty: KTY,
|
82
|
+
crv: 'Ed25519',
|
83
|
+
x: ::JWT::Base64.url_encode(verify_key.to_bytes)
|
84
|
+
}
|
85
|
+
|
86
|
+
if signing_key
|
87
|
+
params[:d] = ::JWT::Base64.url_encode(signing_key.to_bytes)
|
88
|
+
end
|
89
|
+
|
90
|
+
params
|
91
|
+
end
|
92
|
+
|
93
|
+
def verify_key_from_parameters
|
94
|
+
RbNaCl::Signatures::Ed25519::VerifyKey.new(::JWT::Base64.url_decode(self[:x]))
|
95
|
+
end
|
96
|
+
|
97
|
+
def signing_key_from_parameters
|
98
|
+
return nil unless self[:d]
|
99
|
+
|
100
|
+
RbNaCl::Signatures::Ed25519::SigningKey.new(::JWT::Base64.url_decode(self[:d]))
|
101
|
+
end
|
102
|
+
|
103
|
+
class << self
|
104
|
+
def import(jwk_data)
|
105
|
+
new(jwk_data)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/jwt/jwk/rsa.rb
CHANGED
@@ -1,46 +1,202 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'forwardable'
|
4
|
-
|
5
3
|
module JWT
|
6
4
|
module JWK
|
7
|
-
class RSA
|
8
|
-
|
5
|
+
class RSA < KeyBase # rubocop:disable Metrics/ClassLength
|
6
|
+
BINARY = 2
|
7
|
+
KTY = 'RSA'
|
8
|
+
KTYS = [KTY, OpenSSL::PKey::RSA, JWT::JWK::RSA].freeze
|
9
|
+
RSA_PUBLIC_KEY_ELEMENTS = %i[kty n e].freeze
|
10
|
+
RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze
|
11
|
+
RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze
|
9
12
|
|
10
|
-
|
13
|
+
RSA_OPT_PARAMS = %i[p q dp dq qi].freeze
|
14
|
+
RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2
|
11
15
|
|
12
|
-
|
16
|
+
def initialize(key, params = nil, options = {})
|
17
|
+
params ||= {}
|
13
18
|
|
14
|
-
|
15
|
-
|
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)
|
26
|
+
|
27
|
+
super(options, key_params.merge(params))
|
28
|
+
end
|
16
29
|
|
17
|
-
def
|
18
|
-
|
30
|
+
def keypair
|
31
|
+
rsa_key
|
32
|
+
end
|
33
|
+
|
34
|
+
def private?
|
35
|
+
rsa_key.private?
|
36
|
+
end
|
37
|
+
|
38
|
+
def public_key
|
39
|
+
rsa_key.public_key
|
40
|
+
end
|
41
|
+
|
42
|
+
def signing_key
|
43
|
+
rsa_key if private?
|
44
|
+
end
|
19
45
|
|
20
|
-
|
46
|
+
def verify_key
|
47
|
+
rsa_key.public_key
|
21
48
|
end
|
22
49
|
|
23
|
-
def
|
50
|
+
def export(options = {})
|
51
|
+
exported = parameters.clone
|
52
|
+
exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
53
|
+
exported
|
54
|
+
end
|
55
|
+
|
56
|
+
def members
|
57
|
+
RSA_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
58
|
+
end
|
59
|
+
|
60
|
+
def key_digest
|
24
61
|
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
|
25
62
|
OpenSSL::ASN1::Integer.new(public_key.e)])
|
26
63
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
27
64
|
end
|
28
65
|
|
29
|
-
def
|
66
|
+
def []=(key, value)
|
67
|
+
if RSA_KEY_ELEMENTS.include?(key.to_sym)
|
68
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
69
|
+
end
|
70
|
+
|
71
|
+
super(key, value)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def rsa_key
|
77
|
+
@rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_key_params(key)
|
81
|
+
case key
|
82
|
+
when JWT::JWK::RSA
|
83
|
+
key.export(include_private: true)
|
84
|
+
when OpenSSL::PKey::RSA # Accept OpenSSL key as input
|
85
|
+
@rsa_key = key # Preserve the object to avoid recreation
|
86
|
+
parse_rsa_key(key)
|
87
|
+
when Hash
|
88
|
+
key.transform_keys(&:to_sym)
|
89
|
+
else
|
90
|
+
raise ArgumentError, 'key must be of type OpenSSL::PKey::RSA 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 (RSA_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 RSA' unless key_params[:n] && key_params[:e]
|
98
|
+
end
|
99
|
+
|
100
|
+
def parse_rsa_key(key)
|
30
101
|
{
|
31
102
|
kty: KTY,
|
32
|
-
n:
|
33
|
-
e:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
103
|
+
n: encode_open_ssl_bn(key.n),
|
104
|
+
e: encode_open_ssl_bn(key.e),
|
105
|
+
d: encode_open_ssl_bn(key.d),
|
106
|
+
p: encode_open_ssl_bn(key.p),
|
107
|
+
q: encode_open_ssl_bn(key.q),
|
108
|
+
dp: encode_open_ssl_bn(key.dmp1),
|
109
|
+
dq: encode_open_ssl_bn(key.dmq1),
|
110
|
+
qi: encode_open_ssl_bn(key.iqmp)
|
111
|
+
}.compact
|
112
|
+
end
|
113
|
+
|
114
|
+
def jwk_attributes(*attributes)
|
115
|
+
attributes.each_with_object({}) do |attribute, hash|
|
116
|
+
hash[attribute] = decode_open_ssl_bn(self[attribute])
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def encode_open_ssl_bn(key_part)
|
121
|
+
return unless key_part
|
122
|
+
|
123
|
+
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
124
|
+
end
|
125
|
+
|
126
|
+
def decode_open_ssl_bn(jwk_data)
|
127
|
+
self.class.decode_open_ssl_bn(jwk_data)
|
128
|
+
end
|
129
|
+
|
130
|
+
class << self
|
131
|
+
def import(jwk_data)
|
132
|
+
new(jwk_data)
|
133
|
+
end
|
134
|
+
|
135
|
+
def decode_open_ssl_bn(jwk_data)
|
136
|
+
return nil unless jwk_data
|
137
|
+
|
138
|
+
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
|
139
|
+
end
|
140
|
+
|
141
|
+
def create_rsa_key_using_der(rsa_parameters)
|
142
|
+
validate_rsa_parameters!(rsa_parameters)
|
143
|
+
|
144
|
+
sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr|
|
145
|
+
next if rsa_parameters[key].nil?
|
146
|
+
|
147
|
+
arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
|
148
|
+
end
|
149
|
+
|
150
|
+
if sequence.size > 2 # Append "two-prime" version for private key
|
151
|
+
sequence.unshift(OpenSSL::ASN1::Integer.new(0))
|
152
|
+
|
153
|
+
raise JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined' if sequence.size < RSA_ASN1_SEQUENCE.size
|
154
|
+
end
|
155
|
+
|
156
|
+
OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
|
157
|
+
end
|
158
|
+
|
159
|
+
def create_rsa_key_using_sets(rsa_parameters)
|
160
|
+
validate_rsa_parameters!(rsa_parameters)
|
161
|
+
|
162
|
+
OpenSSL::PKey::RSA.new.tap do |rsa_key|
|
163
|
+
rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
|
164
|
+
rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
|
165
|
+
rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize
|
170
|
+
validate_rsa_parameters!(rsa_parameters)
|
171
|
+
|
172
|
+
OpenSSL::PKey::RSA.new.tap do |rsa_key|
|
173
|
+
rsa_key.n = rsa_parameters[:n]
|
174
|
+
rsa_key.e = rsa_parameters[:e]
|
175
|
+
rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
|
176
|
+
rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
|
177
|
+
rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
|
178
|
+
rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
|
179
|
+
rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
|
180
|
+
rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def validate_rsa_parameters!(rsa_parameters)
|
185
|
+
return unless rsa_parameters.key?(:d)
|
186
|
+
|
187
|
+
parameters = RSA_OPT_PARAMS - rsa_parameters.keys
|
188
|
+
return if parameters.empty? || parameters.size == RSA_OPT_PARAMS.size
|
189
|
+
|
190
|
+
raise JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined' # https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2
|
191
|
+
end
|
192
|
+
|
193
|
+
if ::JWT.openssl_3?
|
194
|
+
alias create_rsa_key create_rsa_key_using_der
|
195
|
+
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
|
196
|
+
alias create_rsa_key create_rsa_key_using_sets
|
197
|
+
else
|
198
|
+
alias create_rsa_key create_rsa_key_using_accessors
|
199
|
+
end
|
44
200
|
end
|
45
201
|
end
|
46
202
|
end
|
data/lib/jwt/jwk/set.rb
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
module JWK
|
7
|
+
class Set
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
attr_reader :keys
|
12
|
+
|
13
|
+
def initialize(jwks = nil, options = {}) # rubocop:disable Metrics/CyclomaticComplexity
|
14
|
+
jwks ||= {}
|
15
|
+
|
16
|
+
@keys = case jwks
|
17
|
+
when JWT::JWK::Set # Simple duplication
|
18
|
+
jwks.keys
|
19
|
+
when JWT::JWK::KeyBase # Singleton
|
20
|
+
[jwks]
|
21
|
+
when Hash
|
22
|
+
jwks = jwks.transform_keys(&:to_sym)
|
23
|
+
[*jwks[:keys]].map { |k| JWT::JWK.new(k, nil, options) }
|
24
|
+
when Array
|
25
|
+
jwks.map { |k| JWT::JWK.new(k, nil, options) }
|
26
|
+
else
|
27
|
+
raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def export(options = {})
|
32
|
+
{ keys: @keys.map { |k| k.export(options) } }
|
33
|
+
end
|
34
|
+
|
35
|
+
def_delegators :@keys, :each, :size, :delete, :dig
|
36
|
+
|
37
|
+
def select!(&block)
|
38
|
+
return @keys.select! unless block
|
39
|
+
|
40
|
+
self if @keys.select!(&block)
|
41
|
+
end
|
42
|
+
|
43
|
+
def reject!(&block)
|
44
|
+
return @keys.reject! unless block
|
45
|
+
|
46
|
+
self if @keys.reject!(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def uniq!(&block)
|
50
|
+
self if @keys.uniq!(&block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def merge(enum)
|
54
|
+
@keys += JWT::JWK::Set.new(enum.to_a).keys
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def union(enum)
|
59
|
+
dup.merge(enum)
|
60
|
+
end
|
61
|
+
|
62
|
+
def add(key)
|
63
|
+
@keys << JWT::JWK.new(key)
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def ==(other)
|
68
|
+
other.is_a?(JWT::JWK::Set) && keys.sort == other.keys.sort
|
69
|
+
end
|
70
|
+
|
71
|
+
alias eql? ==
|
72
|
+
alias filter! select!
|
73
|
+
alias length size
|
74
|
+
# For symbolic manipulation
|
75
|
+
alias | union
|
76
|
+
alias + union
|
77
|
+
alias << add
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWK
|
5
|
+
# https://tools.ietf.org/html/rfc7638
|
6
|
+
class Thumbprint
|
7
|
+
attr_reader :jwk
|
8
|
+
|
9
|
+
def initialize(jwk)
|
10
|
+
@jwk = jwk
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
::Base64.urlsafe_encode64(
|
15
|
+
Digest::SHA256.digest(
|
16
|
+
JWT::JSON.generate(
|
17
|
+
jwk.members.sort.to_h
|
18
|
+
)
|
19
|
+
), padding: false
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
alias to_s generate
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/jwt/jwk.rb
CHANGED
@@ -1,31 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative 'jwk/rsa'
|
4
3
|
require_relative 'jwk/key_finder'
|
4
|
+
require_relative 'jwk/set'
|
5
5
|
|
6
6
|
module JWT
|
7
7
|
module JWK
|
8
|
-
MAPPINGS = {
|
9
|
-
'RSA' => ::JWT::JWK::RSA,
|
10
|
-
OpenSSL::PKey::RSA => ::JWT::JWK::RSA
|
11
|
-
}.freeze
|
12
|
-
|
13
8
|
class << self
|
14
|
-
def
|
15
|
-
|
9
|
+
def create_from(key, params = nil, options = {})
|
10
|
+
if key.is_a?(Hash)
|
11
|
+
jwk_kty = key[:kty] || key['kty']
|
12
|
+
raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
|
16
13
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
14
|
+
return mappings.fetch(jwk_kty.to_s) do |kty|
|
15
|
+
raise JWT::JWKError, "Key type #{kty} not supported"
|
16
|
+
end.new(key, params, options)
|
17
|
+
end
|
21
18
|
|
22
|
-
|
23
|
-
MAPPINGS.fetch(keypair.class) do |klass|
|
19
|
+
mappings.fetch(key.class) do |klass|
|
24
20
|
raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
|
25
|
-
end.new(
|
21
|
+
end.new(key, params, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def classes
|
25
|
+
@mappings = nil # reset the cached mappings
|
26
|
+
@classes ||= []
|
26
27
|
end
|
27
28
|
|
28
29
|
alias new create_from
|
30
|
+
alias import create_from
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def mappings
|
35
|
+
@mappings ||= generate_mappings
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_mappings
|
39
|
+
classes.each_with_object({}) do |klass, hash|
|
40
|
+
next unless klass.const_defined?('KTYS')
|
41
|
+
|
42
|
+
Array(klass::KTYS).each do |kty|
|
43
|
+
hash[kty] = klass
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
29
47
|
end
|
30
48
|
end
|
31
49
|
end
|
50
|
+
|
51
|
+
require_relative 'jwk/key_base'
|
52
|
+
require_relative 'jwk/ec'
|
53
|
+
require_relative 'jwk/rsa'
|
54
|
+
require_relative 'jwk/hmac'
|
55
|
+
require_relative 'jwk/okp_rbnacl' if JWT.rbnacl?
|
data/lib/jwt/verify.rb
CHANGED
@@ -10,7 +10,7 @@ module JWT
|
|
10
10
|
}.freeze
|
11
11
|
|
12
12
|
class << self
|
13
|
-
%w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method_name|
|
13
|
+
%w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].each do |method_name|
|
14
14
|
define_method method_name do |payload, options|
|
15
15
|
new(payload, options).send(method_name)
|
16
16
|
end
|
@@ -19,6 +19,7 @@ module JWT
|
|
19
19
|
def verify_claims(payload, options)
|
20
20
|
options.each do |key, val|
|
21
21
|
next unless key.to_s =~ /verify/
|
22
|
+
|
22
23
|
Verify.send(key, payload, options) if val
|
23
24
|
end
|
24
25
|
end
|
@@ -37,12 +38,12 @@ module JWT
|
|
37
38
|
end
|
38
39
|
|
39
40
|
def verify_expiration
|
40
|
-
return unless @payload
|
41
|
+
return unless contains_key?(@payload, 'exp')
|
41
42
|
raise(JWT::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
|
42
43
|
end
|
43
44
|
|
44
45
|
def verify_iat
|
45
|
-
return unless @payload
|
46
|
+
return unless contains_key?(@payload, 'iat')
|
46
47
|
|
47
48
|
iat = @payload['iat']
|
48
49
|
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > Time.now.to_f
|
@@ -53,9 +54,14 @@ module JWT
|
|
53
54
|
|
54
55
|
iss = @payload['iss']
|
55
56
|
|
56
|
-
|
57
|
+
options_iss = Array(options_iss).map { |item| item.is_a?(Symbol) ? item.to_s : item }
|
57
58
|
|
58
|
-
|
59
|
+
case iss
|
60
|
+
when *options_iss
|
61
|
+
nil
|
62
|
+
else
|
63
|
+
raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
|
64
|
+
end
|
59
65
|
end
|
60
66
|
|
61
67
|
def verify_jti
|
@@ -71,16 +77,25 @@ module JWT
|
|
71
77
|
end
|
72
78
|
|
73
79
|
def verify_not_before
|
74
|
-
return unless @payload
|
80
|
+
return unless contains_key?(@payload, 'nbf')
|
75
81
|
raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
|
76
82
|
end
|
77
83
|
|
78
84
|
def verify_sub
|
79
85
|
return unless (options_sub = @options[:sub])
|
86
|
+
|
80
87
|
sub = @payload['sub']
|
81
88
|
raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
|
82
89
|
end
|
83
90
|
|
91
|
+
def verify_required_claims
|
92
|
+
return unless (options_required_claims = @options[:required_claims])
|
93
|
+
|
94
|
+
options_required_claims.each do |required_claim|
|
95
|
+
raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless contains_key?(@payload, required_claim)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
84
99
|
private
|
85
100
|
|
86
101
|
def global_leeway
|
@@ -94,5 +109,9 @@ module JWT
|
|
94
109
|
def nbf_leeway
|
95
110
|
@options[:nbf_leeway] || global_leeway
|
96
111
|
end
|
112
|
+
|
113
|
+
def contains_key?(payload, key)
|
114
|
+
payload.respond_to?(:key?) && payload.key?(key)
|
115
|
+
end
|
97
116
|
end
|
98
117
|
end
|