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.
- checksums.yaml +4 -4
- data/AUTHORS +60 -53
- data/CHANGELOG.md +194 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +360 -106
- data/lib/jwt/base64.rb +19 -2
- data/lib/jwt/claims/audience.rb +30 -0
- data/lib/jwt/claims/crit.rb +35 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +32 -0
- data/lib/jwt/claims/issued_at.rb +22 -0
- data/lib/jwt/claims/issuer.rb +34 -0
- data/lib/jwt/claims/jwt_id.rb +35 -0
- data/lib/jwt/claims/not_before.rb +32 -0
- data/lib/jwt/claims/numeric.rb +77 -0
- data/lib/jwt/claims/required.rb +33 -0
- data/lib/jwt/claims/subject.rb +30 -0
- data/lib/jwt/claims/verification_methods.rb +20 -0
- data/lib/jwt/claims/verifier.rb +61 -0
- data/lib/jwt/claims.rb +74 -0
- data/lib/jwt/claims_validator.rb +7 -24
- data/lib/jwt/configuration/container.rb +52 -0
- data/lib/jwt/configuration/decode_configuration.rb +70 -0
- data/lib/jwt/configuration/jwk_configuration.rb +28 -0
- data/lib/jwt/configuration.rb +23 -0
- data/lib/jwt/decode.rb +70 -61
- data/lib/jwt/deprecations.rb +49 -0
- data/lib/jwt/encode.rb +18 -57
- data/lib/jwt/encoded_token.rb +139 -0
- data/lib/jwt/error.rb +36 -0
- data/lib/jwt/json.rb +1 -1
- data/lib/jwt/jwa/compat.rb +32 -0
- data/lib/jwt/jwa/ecdsa.rb +90 -0
- data/lib/jwt/jwa/eddsa.rb +35 -0
- data/lib/jwt/jwa/hmac.rb +82 -0
- data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
- data/lib/jwt/jwa/none.rb +24 -0
- data/lib/jwt/jwa/ps.rb +35 -0
- data/lib/jwt/jwa/rsa.rb +35 -0
- data/lib/jwt/jwa/signing_algorithm.rb +63 -0
- data/lib/jwt/jwa/unsupported.rb +20 -0
- data/lib/jwt/jwa/wrapper.rb +44 -0
- data/lib/jwt/jwa.rb +58 -0
- data/lib/jwt/jwk/ec.rb +163 -63
- data/lib/jwt/jwk/hmac.rb +68 -24
- data/lib/jwt/jwk/key_base.rb +46 -6
- data/lib/jwt/jwk/key_finder.rb +20 -35
- data/lib/jwt/jwk/kid_as_key_digest.rb +16 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +109 -0
- data/lib/jwt/jwk/rsa.rb +141 -54
- data/lib/jwt/jwk/set.rb +82 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +16 -11
- data/lib/jwt/token.rb +112 -0
- data/lib/jwt/verify.rb +16 -81
- data/lib/jwt/version.rb +53 -11
- data/lib/jwt/x5c_key_finder.rb +52 -0
- data/lib/jwt.rb +28 -4
- data/ruby-jwt.gemspec +15 -5
- metadata +75 -28
- data/.github/workflows/test.yml +0 -74
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -97
- data/.rubocop_todo.yml +0 -185
- data/.sourcelevel.yml +0 -18
- data/Appraisals +0 -10
- data/Gemfile +0 -5
- data/Rakefile +0 -14
- data/lib/jwt/algos/ecdsa.rb +0 -35
- data/lib/jwt/algos/eddsa.rb +0 -30
- data/lib/jwt/algos/hmac.rb +0 -34
- data/lib/jwt/algos/none.rb +0 -15
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -19
- data/lib/jwt/algos/unsupported.rb +0 -17
- data/lib/jwt/algos.rb +0 -44
- data/lib/jwt/default_options.rb +0 -16
- data/lib/jwt/security_utils.rb +0 -57
- data/lib/jwt/signature.rb +0 -39
data/lib/jwt/jwk/rsa.rb
CHANGED
@@ -2,95 +2,173 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWK
|
5
|
-
|
5
|
+
# JSON Web Key (JWK) representation of a RSA key
|
6
|
+
class RSA < KeyBase # rubocop:disable Metrics/ClassLength
|
6
7
|
BINARY = 2
|
7
|
-
KTY = 'RSA'
|
8
|
-
KTYS = [KTY, OpenSSL::PKey::RSA].freeze
|
9
|
-
|
8
|
+
KTY = 'RSA'
|
9
|
+
KTYS = [KTY, OpenSSL::PKey::RSA, JWT::JWK::RSA].freeze
|
10
|
+
RSA_PUBLIC_KEY_ELEMENTS = %i[kty n e].freeze
|
11
|
+
RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze
|
12
|
+
RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
RSA_OPT_PARAMS = %i[p q dp dq qi].freeze
|
15
|
+
RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2
|
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)
|
27
|
+
|
28
|
+
super(options, key_params.merge(params))
|
29
|
+
end
|
30
|
+
|
31
|
+
def keypair
|
32
|
+
rsa_key
|
14
33
|
end
|
15
34
|
|
16
35
|
def private?
|
17
|
-
|
36
|
+
rsa_key.private?
|
18
37
|
end
|
19
38
|
|
20
39
|
def public_key
|
21
|
-
|
40
|
+
rsa_key.public_key
|
22
41
|
end
|
23
42
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
27
|
-
n: encode_open_ssl_bn(public_key.n),
|
28
|
-
e: encode_open_ssl_bn(public_key.e),
|
29
|
-
kid: kid
|
30
|
-
}
|
43
|
+
def signing_key
|
44
|
+
rsa_key if private?
|
45
|
+
end
|
31
46
|
|
32
|
-
|
47
|
+
def verify_key
|
48
|
+
rsa_key.public_key
|
49
|
+
end
|
33
50
|
|
34
|
-
|
51
|
+
def export(options = {})
|
52
|
+
exported = parameters.clone
|
53
|
+
exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
54
|
+
exported
|
35
55
|
end
|
36
56
|
|
37
|
-
|
57
|
+
def members
|
58
|
+
RSA_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
59
|
+
end
|
38
60
|
|
39
|
-
def
|
61
|
+
def key_digest
|
40
62
|
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
|
41
63
|
OpenSSL::ASN1::Integer.new(public_key.e)])
|
42
64
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
43
65
|
end
|
44
66
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
67
|
+
def []=(key, value)
|
68
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' if RSA_KEY_ELEMENTS.include?(key.to_sym)
|
69
|
+
|
70
|
+
super(key, value)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def rsa_key
|
76
|
+
@rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
|
77
|
+
end
|
78
|
+
|
79
|
+
def extract_key_params(key)
|
80
|
+
case key
|
81
|
+
when JWT::JWK::RSA
|
82
|
+
key.export(include_private: true)
|
83
|
+
when OpenSSL::PKey::RSA # Accept OpenSSL key as input
|
84
|
+
@rsa_key = key # Preserve the object to avoid recreation
|
85
|
+
parse_rsa_key(key)
|
86
|
+
when Hash
|
87
|
+
key.transform_keys(&:to_sym)
|
88
|
+
else
|
89
|
+
raise ArgumentError, 'key must be of type OpenSSL::PKey::RSA or Hash with key parameters'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def check_jwk_params!(key_params, params)
|
94
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty?
|
95
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
|
96
|
+
raise JWT::JWKError, 'Key format is invalid for RSA' unless key_params[:n] && key_params[:e]
|
97
|
+
end
|
98
|
+
|
99
|
+
def parse_rsa_key(key)
|
100
|
+
{
|
101
|
+
kty: KTY,
|
102
|
+
n: encode_open_ssl_bn(key.n),
|
103
|
+
e: encode_open_ssl_bn(key.e),
|
104
|
+
d: encode_open_ssl_bn(key.d),
|
105
|
+
p: encode_open_ssl_bn(key.p),
|
106
|
+
q: encode_open_ssl_bn(key.q),
|
107
|
+
dp: encode_open_ssl_bn(key.dmp1),
|
108
|
+
dq: encode_open_ssl_bn(key.dmq1),
|
109
|
+
qi: encode_open_ssl_bn(key.iqmp)
|
110
|
+
}.compact
|
111
|
+
end
|
112
|
+
|
113
|
+
def jwk_attributes(*attributes)
|
114
|
+
attributes.each_with_object({}) do |attribute, hash|
|
115
|
+
hash[attribute] = decode_open_ssl_bn(self[attribute])
|
116
|
+
end
|
54
117
|
end
|
55
118
|
|
56
119
|
def encode_open_ssl_bn(key_part)
|
120
|
+
return unless key_part
|
121
|
+
|
57
122
|
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
58
123
|
end
|
59
124
|
|
125
|
+
def decode_open_ssl_bn(jwk_data)
|
126
|
+
self.class.decode_open_ssl_bn(jwk_data)
|
127
|
+
end
|
128
|
+
|
60
129
|
class << self
|
61
130
|
def import(jwk_data)
|
62
|
-
|
63
|
-
decode_open_ssl_bn(value)
|
64
|
-
end
|
65
|
-
kid = jwk_attributes(jwk_data, :kid)[:kid]
|
66
|
-
self.new(rsa_pkey(pkey_params), kid)
|
131
|
+
new(jwk_data)
|
67
132
|
end
|
68
133
|
|
69
|
-
|
134
|
+
def decode_open_ssl_bn(jwk_data)
|
135
|
+
return nil unless jwk_data
|
70
136
|
|
71
|
-
|
72
|
-
attributes.each_with_object({}) do |attribute, hash|
|
73
|
-
value = jwk_data[attribute] || jwk_data[attribute.to_s]
|
74
|
-
value = yield(value) if block_given?
|
75
|
-
hash[attribute] = value
|
76
|
-
end
|
137
|
+
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
|
77
138
|
end
|
78
139
|
|
79
|
-
def
|
80
|
-
|
140
|
+
def create_rsa_key_using_der(rsa_parameters)
|
141
|
+
validate_rsa_parameters!(rsa_parameters)
|
81
142
|
|
82
|
-
|
143
|
+
sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr|
|
144
|
+
next if rsa_parameters[key].nil?
|
145
|
+
|
146
|
+
arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
|
147
|
+
end
|
148
|
+
|
149
|
+
if sequence.size > 2 # Append "two-prime" version for private key
|
150
|
+
sequence.unshift(OpenSSL::ASN1::Integer.new(0))
|
151
|
+
|
152
|
+
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
|
153
|
+
end
|
154
|
+
|
155
|
+
OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
|
83
156
|
end
|
84
157
|
|
85
|
-
|
86
|
-
|
158
|
+
def create_rsa_key_using_sets(rsa_parameters)
|
159
|
+
validate_rsa_parameters!(rsa_parameters)
|
160
|
+
|
161
|
+
OpenSSL::PKey::RSA.new.tap do |rsa_key|
|
87
162
|
rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
|
88
163
|
rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
|
89
164
|
rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
|
90
|
-
rsa_key
|
91
165
|
end
|
92
|
-
|
93
|
-
|
166
|
+
end
|
167
|
+
|
168
|
+
def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize
|
169
|
+
validate_rsa_parameters!(rsa_parameters)
|
170
|
+
|
171
|
+
OpenSSL::PKey::RSA.new.tap do |rsa_key|
|
94
172
|
rsa_key.n = rsa_parameters[:n]
|
95
173
|
rsa_key.e = rsa_parameters[:e]
|
96
174
|
rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
|
@@ -99,15 +177,24 @@ module JWT
|
|
99
177
|
rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
|
100
178
|
rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
|
101
179
|
rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
|
102
|
-
|
103
|
-
rsa_key
|
104
180
|
end
|
105
181
|
end
|
106
182
|
|
107
|
-
def
|
108
|
-
return
|
183
|
+
def validate_rsa_parameters!(rsa_parameters)
|
184
|
+
return unless rsa_parameters.key?(:d)
|
109
185
|
|
110
|
-
|
186
|
+
parameters = RSA_OPT_PARAMS - rsa_parameters.keys
|
187
|
+
return if parameters.empty? || parameters.size == RSA_OPT_PARAMS.size
|
188
|
+
|
189
|
+
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
|
190
|
+
end
|
191
|
+
|
192
|
+
if ::JWT.openssl_3?
|
193
|
+
alias create_rsa_key create_rsa_key_using_der
|
194
|
+
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
|
195
|
+
alias create_rsa_key create_rsa_key_using_sets
|
196
|
+
else
|
197
|
+
alias create_rsa_key create_rsa_key_using_accessors
|
111
198
|
end
|
112
199
|
end
|
113
200
|
end
|
data/lib/jwt/jwk/set.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
module JWK
|
7
|
+
# JSON Web Key Set (JWKS) representation
|
8
|
+
# https://tools.ietf.org/html/rfc7517
|
9
|
+
class Set
|
10
|
+
include Enumerable
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
attr_reader :keys
|
14
|
+
|
15
|
+
def initialize(jwks = nil, options = {}) # rubocop:disable Metrics/CyclomaticComplexity
|
16
|
+
jwks ||= {}
|
17
|
+
|
18
|
+
@keys = case jwks
|
19
|
+
when JWT::JWK::Set # Simple duplication
|
20
|
+
jwks.keys
|
21
|
+
when JWT::JWK::KeyBase # Singleton
|
22
|
+
[jwks]
|
23
|
+
when Hash
|
24
|
+
jwks = jwks.transform_keys(&:to_sym)
|
25
|
+
[*jwks[:keys]].map { |k| JWT::JWK.new(k, nil, options) }
|
26
|
+
when Array
|
27
|
+
jwks.map { |k| JWT::JWK.new(k, nil, options) }
|
28
|
+
else
|
29
|
+
raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def export(options = {})
|
34
|
+
{ keys: @keys.map { |k| k.export(options) } }
|
35
|
+
end
|
36
|
+
|
37
|
+
def_delegators :@keys, :each, :size, :delete, :dig
|
38
|
+
|
39
|
+
def select!(&block)
|
40
|
+
return @keys.select! unless block
|
41
|
+
|
42
|
+
self if @keys.select!(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def reject!(&block)
|
46
|
+
return @keys.reject! unless block
|
47
|
+
|
48
|
+
self if @keys.reject!(&block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def uniq!(&block)
|
52
|
+
self if @keys.uniq!(&block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge(enum)
|
56
|
+
@keys += JWT::JWK::Set.new(enum.to_a).keys
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def union(enum)
|
61
|
+
dup.merge(enum)
|
62
|
+
end
|
63
|
+
|
64
|
+
def add(key)
|
65
|
+
@keys << JWT::JWK.new(key)
|
66
|
+
self
|
67
|
+
end
|
68
|
+
|
69
|
+
def ==(other)
|
70
|
+
other.is_a?(JWT::JWK::Set) && keys.sort == other.keys.sort
|
71
|
+
end
|
72
|
+
|
73
|
+
alias eql? ==
|
74
|
+
alias filter! select!
|
75
|
+
alias length size
|
76
|
+
# For symbolic manipulation
|
77
|
+
alias | union
|
78
|
+
alias + union
|
79
|
+
alias << add
|
80
|
+
end
|
81
|
+
end
|
82
|
+
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,23 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'jwk/key_finder'
|
4
|
+
require_relative 'jwk/set'
|
4
5
|
|
5
6
|
module JWT
|
7
|
+
# JSON Web Key (JWK)
|
6
8
|
module JWK
|
7
9
|
class << self
|
8
|
-
def
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
def create_from(key, params = nil, options = {})
|
11
|
+
if key.is_a?(Hash)
|
12
|
+
jwk_kty = key[:kty] || key['kty']
|
13
|
+
raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
|
14
|
+
|
15
|
+
return mappings.fetch(jwk_kty.to_s) do |kty|
|
16
|
+
raise JWT::JWKError, "Key type #{kty} not supported"
|
17
|
+
end.new(key, params, options)
|
18
|
+
end
|
16
19
|
|
17
|
-
|
18
|
-
mappings.fetch(keypair.class) do |klass|
|
20
|
+
mappings.fetch(key.class) do |klass|
|
19
21
|
raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
|
20
|
-
end.new(
|
22
|
+
end.new(key, params, options)
|
21
23
|
end
|
22
24
|
|
23
25
|
def classes
|
@@ -26,6 +28,7 @@ module JWT
|
|
26
28
|
end
|
27
29
|
|
28
30
|
alias new create_from
|
31
|
+
alias import create_from
|
29
32
|
|
30
33
|
private
|
31
34
|
|
@@ -36,6 +39,7 @@ module JWT
|
|
36
39
|
def generate_mappings
|
37
40
|
classes.each_with_object({}) do |klass, hash|
|
38
41
|
next unless klass.const_defined?('KTYS')
|
42
|
+
|
39
43
|
Array(klass::KTYS).each do |kty|
|
40
44
|
hash[kty] = klass
|
41
45
|
end
|
@@ -49,3 +53,4 @@ require_relative 'jwk/key_base'
|
|
49
53
|
require_relative 'jwk/ec'
|
50
54
|
require_relative 'jwk/rsa'
|
51
55
|
require_relative 'jwk/hmac'
|
56
|
+
require_relative 'jwk/okp_rbnacl' if JWT.rbnacl?
|
data/lib/jwt/token.rb
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
# Represents a JWT token
|
5
|
+
#
|
6
|
+
# Basic token signed using the HS256 algorithm:
|
7
|
+
#
|
8
|
+
# token = JWT::Token.new(payload: {pay: 'load'})
|
9
|
+
# token.sign!(algorithm: 'HS256', key: 'secret')
|
10
|
+
# token.jwt # => eyJhb....
|
11
|
+
#
|
12
|
+
# Custom headers will be combined with generated headers:
|
13
|
+
# token = JWT::Token.new(payload: {pay: 'load'}, header: {custom: "value"})
|
14
|
+
# token.sign!(algorithm: 'HS256', key: 'secret')
|
15
|
+
# token.header # => {"custom"=>"value", "alg"=>"HS256"}
|
16
|
+
#
|
17
|
+
class Token
|
18
|
+
include Claims::VerificationMethods
|
19
|
+
|
20
|
+
# Initializes a new Token instance.
|
21
|
+
#
|
22
|
+
# @param header [Hash] the header of the JWT token.
|
23
|
+
# @param payload [Hash] the payload of the JWT token.
|
24
|
+
def initialize(payload:, header: {})
|
25
|
+
@header = header&.transform_keys(&:to_s)
|
26
|
+
@payload = payload
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the decoded signature of the JWT token.
|
30
|
+
#
|
31
|
+
# @return [String] the decoded signature of the JWT token.
|
32
|
+
def signature
|
33
|
+
@signature ||= ::JWT::Base64.url_decode(encoded_signature || '')
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the encoded signature of the JWT token.
|
37
|
+
#
|
38
|
+
# @return [String] the encoded signature of the JWT token.
|
39
|
+
def encoded_signature
|
40
|
+
@encoded_signature ||= ::JWT::Base64.url_encode(signature)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the decoded header of the JWT token.
|
44
|
+
#
|
45
|
+
# @return [Hash] the header of the JWT token.
|
46
|
+
attr_reader :header
|
47
|
+
|
48
|
+
# Returns the encoded header of the JWT token.
|
49
|
+
#
|
50
|
+
# @return [String] the encoded header of the JWT token.
|
51
|
+
def encoded_header
|
52
|
+
@encoded_header ||= ::JWT::Base64.url_encode(JWT::JSON.generate(header))
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the payload of the JWT token.
|
56
|
+
#
|
57
|
+
# @return [Hash] the payload of the JWT token.
|
58
|
+
attr_reader :payload
|
59
|
+
|
60
|
+
# Returns the encoded payload of the JWT token.
|
61
|
+
#
|
62
|
+
# @return [String] the encoded payload of the JWT token.
|
63
|
+
def encoded_payload
|
64
|
+
@encoded_payload ||= ::JWT::Base64.url_encode(JWT::JSON.generate(payload))
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns the signing input of the JWT token.
|
68
|
+
#
|
69
|
+
# @return [String] the signing input of the JWT token.
|
70
|
+
def signing_input
|
71
|
+
@signing_input ||= [encoded_header, encoded_payload].join('.')
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the JWT token as a string.
|
75
|
+
#
|
76
|
+
# @return [String] the JWT token as a string.
|
77
|
+
# @raise [JWT::EncodeError] if the token is not signed or other encoding issues
|
78
|
+
def jwt
|
79
|
+
@jwt ||= (@signature && [encoded_header, @detached_payload ? '' : encoded_payload, encoded_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed')
|
80
|
+
end
|
81
|
+
|
82
|
+
# Detaches the payload according to https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
|
83
|
+
#
|
84
|
+
def detach_payload!
|
85
|
+
@detached_payload = true
|
86
|
+
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
# Signs the JWT token.
|
91
|
+
#
|
92
|
+
# @param algorithm [String, Object] the algorithm to use for signing.
|
93
|
+
# @param key [String] the key to use for signing.
|
94
|
+
# @return [void]
|
95
|
+
# @raise [JWT::EncodeError] if the token is already signed or other problems when signing
|
96
|
+
def sign!(algorithm:, key:)
|
97
|
+
raise ::JWT::EncodeError, 'Token already signed' if @signature
|
98
|
+
|
99
|
+
JWA.resolve(algorithm).tap do |algo|
|
100
|
+
header.merge!(algo.header)
|
101
|
+
@signature = algo.sign(data: signing_input, signing_key: key)
|
102
|
+
end
|
103
|
+
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns the JWT token as a string.
|
108
|
+
#
|
109
|
+
# @return [String] the JWT token as a string.
|
110
|
+
alias to_s jwt
|
111
|
+
end
|
112
|
+
end
|
data/lib/jwt/verify.rb
CHANGED
@@ -1,105 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative 'error'
|
4
4
|
|
5
5
|
module JWT
|
6
|
-
#
|
6
|
+
# @deprecated This class is deprecated and will be removed in the next major version of ruby-jwt.
|
7
7
|
class Verify
|
8
|
-
DEFAULTS = {
|
9
|
-
|
10
|
-
}.freeze
|
8
|
+
DEFAULTS = { leeway: 0 }.freeze
|
9
|
+
METHODS = %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].freeze
|
11
10
|
|
11
|
+
private_constant(:DEFAULTS, :METHODS)
|
12
12
|
class << self
|
13
|
-
|
14
|
-
define_method
|
13
|
+
METHODS.each do |method_name|
|
14
|
+
define_method(method_name) do |payload, options|
|
15
15
|
new(payload, options).send(method_name)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
# @deprecated This method is deprecated and will be removed in the next major version of ruby-jwt.
|
19
20
|
def verify_claims(payload, options)
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
21
|
+
Deprecations.warning('The ::JWT::Verify.verify_claims method is deprecated and will be removed in the next major version of ruby-jwt')
|
22
|
+
::JWT::Claims.verify!(payload, options)
|
23
|
+
true
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
# @deprecated This class is deprecated and will be removed in the next major version of ruby-jwt.
|
27
28
|
def initialize(payload, options)
|
29
|
+
Deprecations.warning('The ::JWT::Verify class is deprecated and will be removed in the next major version of ruby-jwt')
|
28
30
|
@payload = payload
|
29
31
|
@options = DEFAULTS.merge(options)
|
30
32
|
end
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
aud = @payload['aud']
|
36
|
-
raise(JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{aud || '<none>'}") if ([*aud] & [*options_aud]).empty?
|
37
|
-
end
|
38
|
-
|
39
|
-
def verify_expiration
|
40
|
-
return unless @payload.include?('exp')
|
41
|
-
raise(JWT::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
|
42
|
-
end
|
43
|
-
|
44
|
-
def verify_iat
|
45
|
-
return unless @payload.include?('iat')
|
46
|
-
|
47
|
-
iat = @payload['iat']
|
48
|
-
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > Time.now.to_f
|
49
|
-
end
|
50
|
-
|
51
|
-
def verify_iss
|
52
|
-
return unless (options_iss = @options[:iss])
|
53
|
-
|
54
|
-
iss = @payload['iss']
|
55
|
-
|
56
|
-
return if Array(options_iss).map(&:to_s).include?(iss.to_s)
|
57
|
-
|
58
|
-
raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
|
59
|
-
end
|
60
|
-
|
61
|
-
def verify_jti
|
62
|
-
options_verify_jti = @options[:verify_jti]
|
63
|
-
jti = @payload['jti']
|
64
|
-
|
65
|
-
if options_verify_jti.respond_to?(:call)
|
66
|
-
verified = options_verify_jti.arity == 2 ? options_verify_jti.call(jti, @payload) : options_verify_jti.call(jti)
|
67
|
-
raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
|
68
|
-
elsif jti.to_s.strip.empty?
|
69
|
-
raise(JWT::InvalidJtiError, 'Missing jti')
|
34
|
+
METHODS.each do |method_name|
|
35
|
+
define_method(method_name) do
|
36
|
+
::JWT::Claims.verify!(@payload, @options.merge(method_name => true))
|
70
37
|
end
|
71
38
|
end
|
72
|
-
|
73
|
-
def verify_not_before
|
74
|
-
return unless @payload.include?('nbf')
|
75
|
-
raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
|
76
|
-
end
|
77
|
-
|
78
|
-
def verify_sub
|
79
|
-
return unless (options_sub = @options[:sub])
|
80
|
-
sub = @payload['sub']
|
81
|
-
raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
|
82
|
-
end
|
83
|
-
|
84
|
-
def verify_required_claims
|
85
|
-
return unless (options_required_claims = @options[:required_claims])
|
86
|
-
options_required_claims.each do |required_claim|
|
87
|
-
raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
private
|
92
|
-
|
93
|
-
def global_leeway
|
94
|
-
@options[:leeway]
|
95
|
-
end
|
96
|
-
|
97
|
-
def exp_leeway
|
98
|
-
@options[:exp_leeway] || global_leeway
|
99
|
-
end
|
100
|
-
|
101
|
-
def nbf_leeway
|
102
|
-
@options[:nbf_leeway] || global_leeway
|
103
|
-
end
|
104
39
|
end
|
105
40
|
end
|