jwt 2.10.2 → 3.2.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -41
  3. data/CODE_OF_CONDUCT.md +14 -14
  4. data/CONTRIBUTING.md +11 -12
  5. data/README.md +190 -221
  6. data/UPGRADING.md +47 -0
  7. data/lib/jwt/base64.rb +1 -10
  8. data/lib/jwt/claims/numeric.rb +0 -32
  9. data/lib/jwt/claims.rb +0 -7
  10. data/lib/jwt/configuration/container.rb +0 -1
  11. data/lib/jwt/configuration/decode_configuration.rb +7 -2
  12. data/lib/jwt/decode.rb +25 -16
  13. data/lib/jwt/encoded_token/claims_context.rb +23 -0
  14. data/lib/jwt/encoded_token.rb +97 -14
  15. data/lib/jwt/error.rb +0 -3
  16. data/lib/jwt/jwa/ecdsa.rb +25 -4
  17. data/lib/jwt/jwa/hmac.rb +28 -10
  18. data/lib/jwt/jwa/ps.rb +1 -0
  19. data/lib/jwt/jwa/rsa.rb +1 -0
  20. data/lib/jwt/jwa/signer_context.rb +19 -0
  21. data/lib/jwt/jwa/signing_algorithm.rb +0 -1
  22. data/lib/jwt/jwa/verifier_context.rb +21 -0
  23. data/lib/jwt/jwa.rb +43 -26
  24. data/lib/jwt/jwk/ec.rb +52 -62
  25. data/lib/jwt/jwk/hmac.rb +3 -3
  26. data/lib/jwt/jwk/key_base.rb +15 -1
  27. data/lib/jwt/jwk/key_finder.rb +35 -9
  28. data/lib/jwt/jwk/rsa.rb +6 -2
  29. data/lib/jwt/jwk.rb +0 -1
  30. data/lib/jwt/token.rb +26 -7
  31. data/lib/jwt/version.rb +4 -28
  32. data/lib/jwt/x5c_key_finder.rb +1 -1
  33. data/lib/jwt.rb +1 -7
  34. data/ruby-jwt.gemspec +1 -0
  35. metadata +21 -13
  36. data/lib/jwt/claims/verification_methods.rb +0 -20
  37. data/lib/jwt/claims_validator.rb +0 -18
  38. data/lib/jwt/deprecations.rb +0 -49
  39. data/lib/jwt/jwa/compat.rb +0 -32
  40. data/lib/jwt/jwa/eddsa.rb +0 -35
  41. data/lib/jwt/jwa/hmac_rbnacl.rb +0 -50
  42. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +0 -47
  43. data/lib/jwt/jwa/wrapper.rb +0 -44
  44. data/lib/jwt/jwk/okp_rbnacl.rb +0 -109
  45. data/lib/jwt/verify.rb +0 -40
data/UPGRADING.md ADDED
@@ -0,0 +1,47 @@
1
+ # Upgrading ruby-jwt to >= 3.0.0
2
+
3
+ ## Removal of the indirect [RbNaCl](https://github.com/RubyCrypto/rbnacl) dependency
4
+
5
+ Historically, the set of supported algorithms was extended by including the `rbnacl` gem in the application's Gemfile. On load, ruby-jwt tried to load the gem and, if available, extend the algorithms to those provided by the `rbnacl/libsodium` libraries. This indirect dependency has caused some maintenance pain and confusion about which versions of the gem are supported.
6
+
7
+ Some work to ease the way alternative algorithms can be implemented has been done. This enables the extraction of the algorithm provided by `rbnacl`.
8
+
9
+ The extracted algorithms now live in the [jwt-eddsa](https://rubygems.org/gems/jwt-eddsa) gem.
10
+
11
+ ### Dropped support for HS512256
12
+
13
+ The algorithm HS512256 (HMAC-SHA-512 truncated to 256-bits) is not part of any JWA/JWT RFC and therefore will not be supported anymore. It was part of the HMAC algorithms provided by the indirect [RbNaCl](https://github.com/RubyCrypto/rbnacl) dependency. Currently, there are no direct substitutes for the algorithm.
14
+
15
+ ### `JWT::EncodedToken#payload` will raise before token is verified
16
+
17
+ To avoid accidental use of unverified tokens, the `JWT::EncodedToken#payload` method will raise an error if accessed before the token signature has been verified.
18
+
19
+ To access the payload before verification, use the method `JWT::EncodedToken#unverified_payload`.
20
+
21
+ ## Stricter requirements on Base64 encoded data
22
+
23
+ Base64 decoding will no longer fallback on the looser RFC 2045. The biggest difference is that the looser version was ignoring whitespaces and newlines, whereas the stricter version raises errors in such cases.
24
+
25
+ If you, for example, read tokens from files, there could be problems with trailing newlines. Make sure you trim your input before passing it to the decoding mechanisms.
26
+
27
+ ## Claim verification revamp
28
+
29
+ Claim verification has been [split into separate classes](https://github.com/jwt/ruby-jwt/pull/605) and has [a new API](https://github.com/jwt/ruby-jwt/pull/626), leading to the following deprecations:
30
+
31
+ - The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`.
32
+ - The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`.
33
+ - The `::JWT::JWA.create` method will be removed.
34
+ - The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`.
35
+ - Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
36
+ - Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
37
+
38
+ ## Algorithm restructuring
39
+
40
+ The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes led to a few deprecations and new requirements:
41
+
42
+ - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed.
43
+ - Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module.
44
+
45
+ ## Base64 the `k´ value for HMAC JWKs
46
+
47
+ The gem was missing the Base64 encoding and decoding when representing and parsing a HMAC key as a JWK. This issue is now addressed. The added encoding will break compatibility with JWKs produced by older versions of the gem.
data/lib/jwt/base64.rb CHANGED
@@ -14,22 +14,13 @@ module JWT
14
14
  end
15
15
 
16
16
  # Decode a string with URL-safe Base64 complying with RFC 4648.
17
- # Deprecated support for RFC 2045 remains for now. ("All line breaks or other characters not found in Table 1 must be ignored by decoding software")
18
17
  # @api private
19
18
  def url_decode(str)
20
19
  ::Base64.urlsafe_decode64(str)
21
20
  rescue ArgumentError => e
22
21
  raise unless e.message == 'invalid base64'
23
- raise Base64DecodeError, 'Invalid base64 encoding' if JWT.configuration.strict_base64_decoding
24
22
 
25
- loose_urlsafe_decode64(str).tap do
26
- Deprecations.warning('Invalid base64 input detected, could be because of invalid padding, trailing whitespaces or newline chars. Graceful handling of invalid input will be dropped in the next major version of ruby-jwt', only_if_valid: true)
27
- end
28
- end
29
-
30
- def loose_urlsafe_decode64(str)
31
- str += '=' * (4 - str.length.modulo(4))
32
- ::Base64.decode64(str.tr('-_', '+/'))
23
+ raise Base64DecodeError, 'Invalid base64 encoding'
33
24
  end
34
25
  end
35
26
  end
@@ -5,18 +5,6 @@ module JWT
5
5
  # The Numeric class is responsible for validating numeric claims in a JWT token.
6
6
  # The numeric claims are: exp, iat and nbf
7
7
  class Numeric
8
- # The Compat class provides backward compatibility for numeric claim validation.
9
- # @api private
10
- class Compat
11
- def initialize(payload)
12
- @payload = payload
13
- end
14
-
15
- def verify!
16
- JWT::Claims.verify_payload!(@payload, :numeric)
17
- end
18
- end
19
-
20
8
  # List of numeric claims that can be validated.
21
9
  NUMERIC_CLAIMS = %i[
22
10
  exp
@@ -26,14 +14,6 @@ module JWT
26
14
 
27
15
  private_constant(:NUMERIC_CLAIMS)
28
16
 
29
- # @api private
30
- def self.new(*args)
31
- return super if args.empty?
32
-
33
- Deprecations.warning('Calling ::JWT::Claims::Numeric.new with the payload will be removed in the next major version of ruby-jwt')
34
- Compat.new(*args)
35
- end
36
-
37
17
  # Verifies the numeric claims in the JWT context.
38
18
  #
39
19
  # @param context [Object] the context containing the JWT payload.
@@ -43,18 +23,6 @@ module JWT
43
23
  validate_numeric_claims(context.payload)
44
24
  end
45
25
 
46
- # Verifies the numeric claims in the JWT payload.
47
- #
48
- # @param payload [Hash] the JWT payload containing the claims.
49
- # @param _args [Hash] additional arguments (not used).
50
- # @raise [JWT::InvalidClaimError] if any numeric claim is invalid.
51
- # @return [nil]
52
- # @deprecated The ::JWT::Claims::Numeric.verify! method will be removed in the next major version of ruby-jwt
53
- def self.verify!(payload:, **_args)
54
- Deprecations.warning('The ::JWT::Claims::Numeric.verify! method will be removed in the next major version of ruby-jwt.')
55
- JWT::Claims.verify_payload!(payload, :numeric)
56
- end
57
-
58
26
  private
59
27
 
60
28
  def validate_numeric_claims(payload)
data/lib/jwt/claims.rb CHANGED
@@ -11,7 +11,6 @@ require_relative 'claims/not_before'
11
11
  require_relative 'claims/numeric'
12
12
  require_relative 'claims/required'
13
13
  require_relative 'claims/subject'
14
- require_relative 'claims/verification_methods'
15
14
  require_relative 'claims/verifier'
16
15
 
17
16
  module JWT
@@ -33,12 +32,6 @@ module JWT
33
32
  Error = Struct.new(:message, keyword_init: true)
34
33
 
35
34
  class << self
36
- # @deprecated Use {verify_payload!} instead. Will be removed in the next major version of ruby-jwt.
37
- def verify!(payload, options)
38
- Deprecations.warning('The ::JWT::Claims.verify! method is deprecated will be removed in the next major version of ruby-jwt')
39
- DecodeVerifier.verify!(payload, options)
40
- end
41
-
42
35
  # Checks if the claims in the JWT payload are valid.
43
36
  # @example
44
37
  #
@@ -30,7 +30,6 @@ module JWT
30
30
  def reset!
31
31
  @decode = DecodeConfiguration.new
32
32
  @jwk = JwkConfiguration.new
33
- @strict_base64_decoding = false
34
33
 
35
34
  self.deprecation_warnings = :once
36
35
  end
@@ -24,6 +24,8 @@ module JWT
24
24
  # @return [Array<String>] the list of acceptable algorithms.
25
25
  # @!attribute [rw] required_claims
26
26
  # @return [Array<String>] the list of required claims.
27
+ # @!attribute [rw] enforce_hmac_key_length
28
+ # @return [Boolean] whether to enforce minimum HMAC key lengths. false disables validation (default).
27
29
 
28
30
  attr_accessor :verify_expiration,
29
31
  :verify_not_before,
@@ -34,7 +36,8 @@ module JWT
34
36
  :verify_sub,
35
37
  :leeway,
36
38
  :algorithms,
37
- :required_claims
39
+ :required_claims,
40
+ :enforce_hmac_key_length
38
41
 
39
42
  # Initializes a new DecodeConfiguration instance with default settings.
40
43
  def initialize
@@ -48,6 +51,7 @@ module JWT
48
51
  @leeway = 0
49
52
  @algorithms = ['HS256']
50
53
  @required_claims = []
54
+ @enforce_hmac_key_length = false
51
55
  end
52
56
 
53
57
  # @api private
@@ -62,7 +66,8 @@ module JWT
62
66
  verify_sub: verify_sub,
63
67
  leeway: leeway,
64
68
  algorithms: algorithms,
65
- required_claims: required_claims
69
+ required_claims: required_claims,
70
+ enforce_hmac_key_length: enforce_hmac_key_length
66
71
  }
67
72
  end
68
73
  end
data/lib/jwt/decode.rb CHANGED
@@ -6,6 +6,11 @@ require 'jwt/x5c_key_finder'
6
6
  module JWT
7
7
  # The Decode class is responsible for decoding and verifying JWT tokens.
8
8
  class Decode
9
+ # Order is very important - first check for string keys, next for symbols
10
+ ALGORITHM_KEYS = ['algorithm',
11
+ :algorithm,
12
+ 'algorithms',
13
+ :algorithms].freeze
9
14
  # Initializes a new Decode instance.
10
15
  #
11
16
  # @param jwt [String] the JWT to decode.
@@ -33,10 +38,10 @@ module JWT
33
38
  verify_algo
34
39
  set_key
35
40
  verify_signature
36
- Claims::DecodeVerifier.verify!(token.payload, @options)
41
+ Claims::DecodeVerifier.verify!(token.unverified_payload, @options)
37
42
  end
38
43
 
39
- [token.payload, token.header]
44
+ [token.unverified_payload, token.header]
40
45
  end
41
46
 
42
47
  private
@@ -53,14 +58,21 @@ module JWT
53
58
 
54
59
  def verify_algo
55
60
  raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
56
- raise JWT::DecodeError, 'Token header not a JSON object' unless token.header.is_a?(Hash)
61
+ raise JWT::DecodeError, 'Token header not a JSON object' unless valid_token_header?
57
62
  raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
58
63
  raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
59
64
  end
60
65
 
61
66
  def set_key
62
67
  @key = find_key(&@keyfinder) if @keyfinder
63
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
68
+ if @options[:jwks]
69
+ @key = ::JWT::JWK::KeyFinder.new(
70
+ jwks: @options[:jwks],
71
+ allow_nil_kid: @options[:allow_nil_kid],
72
+ key_fields: @options[:key_fields]
73
+ ).call(token)
74
+ end
75
+
64
76
  return unless (x5c_options = @options[:x5c])
65
77
 
66
78
  @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
@@ -70,18 +82,9 @@ module JWT
70
82
  @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
71
83
  end
72
84
 
73
- # Order is very important - first check for string keys, next for symbols
74
- ALGORITHM_KEYS = ['algorithm',
75
- :algorithm,
76
- 'algorithms',
77
- :algorithms].freeze
78
-
79
85
  def given_algorithms
80
- ALGORITHM_KEYS.each do |alg_key|
81
- alg = @options[alg_key]
82
- return Array(alg) if alg
83
- end
84
- []
86
+ alg_key = ALGORITHM_KEYS.find { |key| @options[key] }
87
+ Array(@options[alg_key])
85
88
  end
86
89
 
87
90
  def allowed_algorithms
@@ -93,7 +96,7 @@ module JWT
93
96
  end
94
97
 
95
98
  def find_key(&keyfinder)
96
- key = (keyfinder.arity == 2 ? yield(token.header, token.payload) : yield(token.header))
99
+ key = (keyfinder.arity == 2 ? yield(token.header, token.unverified_payload) : yield(token.header))
97
100
  # key can be of type [string, nil, OpenSSL::PKey, Array]
98
101
  return key if key && !Array(key).empty?
99
102
 
@@ -110,9 +113,15 @@ module JWT
110
113
  end
111
114
 
112
115
  def none_algorithm?
116
+ return false unless valid_token_header?
117
+
113
118
  alg_in_header == 'none'
114
119
  end
115
120
 
121
+ def valid_token_header?
122
+ token.header.is_a?(Hash)
123
+ end
124
+
116
125
  def alg_in_header
117
126
  token.header['alg']
118
127
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ # @private
7
+ class EncodedToken
8
+ # Allow access to the unverified payload for claim verification.
9
+ class ClaimsContext
10
+ extend Forwardable
11
+
12
+ def_delegators :@token, :header, :unverified_payload
13
+
14
+ def initialize(token)
15
+ @token = token
16
+ end
17
+
18
+ def payload
19
+ unverified_payload
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'encoded_token/claims_context'
4
+
3
5
  module JWT
4
6
  # Represents an encoded JWT token
5
7
  #
@@ -12,7 +14,9 @@ module JWT
12
14
  # encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret')
13
15
  # encoded_token.payload # => {'pay' => 'load'}
14
16
  class EncodedToken
15
- include Claims::VerificationMethods
17
+ DEFAULT_CLAIMS = [:exp].freeze
18
+
19
+ private_constant(:DEFAULT_CLAIMS)
16
20
 
17
21
  # Returns the original token provided to the class.
18
22
  # @return [String] The JWT token.
@@ -26,6 +30,9 @@ module JWT
26
30
  raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String)
27
31
 
28
32
  @jwt = jwt
33
+ @signature_verified = false
34
+ @claims_verified = false
35
+
29
36
  @encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
30
37
  end
31
38
 
@@ -53,11 +60,21 @@ module JWT
53
60
  # @return [String] the encoded header.
54
61
  attr_reader :encoded_header
55
62
 
56
- # Returns the payload of the JWT token.
63
+ # Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
57
64
  #
58
65
  # @return [Hash] the payload.
66
+ # @raise [JWT::DecodeError] if the signature has not been verified.
59
67
  def payload
60
- @payload ||= decode_payload
68
+ raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified
69
+ raise JWT::DecodeError, 'Verify the token claims before accessing the payload' unless @claims_verified
70
+
71
+ decoded_payload
72
+ end
73
+
74
+ # Returns the payload of the JWT token without requiring the signature to have been verified.
75
+ # @return [Hash] the payload.
76
+ def unverified_payload
77
+ decoded_payload
61
78
  end
62
79
 
63
80
  # Sets or returns the encoded payload of the JWT token.
@@ -72,6 +89,33 @@ module JWT
72
89
  [encoded_header, encoded_payload].join('.')
73
90
  end
74
91
 
92
+ # Verifies the token signature and claims.
93
+ # By default it verifies the 'exp' claim.
94
+ #
95
+ # @example
96
+ # encoded_token.verify!(signature: { algorithm: 'HS256', key: 'secret' }, claims: [:exp])
97
+ #
98
+ # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
99
+ # @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
100
+ # @return [nil]
101
+ # @raise [JWT::DecodeError] if the signature or claim verification fails.
102
+ def verify!(signature:, claims: nil)
103
+ verify_signature!(**signature)
104
+ claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims)
105
+ nil
106
+ end
107
+
108
+ # Verifies the token signature and claims.
109
+ # By default it verifies the 'exp' claim.
110
+
111
+ # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
112
+ # @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
113
+ # @return [Boolean] true if the signature and claims are valid, false otherwise.
114
+ def valid?(signature:, claims: nil)
115
+ valid_signature?(**signature) &&
116
+ (claims.is_a?(Array) ? valid_claims?(*claims) : valid_claims?(claims))
117
+ end
118
+
75
119
  # Verifies the signature of the JWT token.
76
120
  #
77
121
  # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
@@ -81,11 +125,7 @@ module JWT
81
125
  # @raise [JWT::VerificationError] if the signature verification fails.
82
126
  # @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
83
127
  def verify_signature!(algorithm:, key: nil, key_finder: nil)
84
- raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
85
-
86
- key ||= key_finder.call(self)
87
-
88
- return if valid_signature?(algorithm: algorithm, key: key)
128
+ return if valid_signature?(algorithm: algorithm, key: key, key_finder: key_finder)
89
129
 
90
130
  raise JWT::VerificationError, 'Signature verification failed'
91
131
  end
@@ -93,20 +133,59 @@ module JWT
93
133
  # Checks if the signature of the JWT token is valid.
94
134
  #
95
135
  # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
96
- # @param key [String, Array<String>] the key(s) to use for verification.
136
+ # @param key [String, Array<String>, JWT::JWK::KeyBase, Array<JWT::JWK::KeyBase>] the key(s) to use for verification.
137
+ # @param key_finder [#call] an object responding to `call` to find the key for verification.
97
138
  # @return [Boolean] true if the signature is valid, false otherwise.
98
- def valid_signature?(algorithm:, key:)
99
- Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo|
100
- Array(key).any? do |one_key|
101
- algo.verify(data: signing_input, signature: signature, verification_key: one_key)
102
- end
139
+ def valid_signature?(algorithm: nil, key: nil, key_finder: nil)
140
+ raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
141
+
142
+ keys = Array(key || key_finder.call(self))
143
+ verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: header['alg'])
144
+
145
+ raise JWT::VerificationError, 'No algorithm provided' if verifiers.empty?
146
+
147
+ valid = verifiers.any? do |jwa|
148
+ jwa.verify(data: signing_input, signature: signature)
149
+ end
150
+ valid.tap { |verified| @signature_verified = verified }
151
+ end
152
+
153
+ # Verifies the claims of the token.
154
+ # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
155
+ # @raise [JWT::DecodeError] if the claims are invalid.
156
+ def verify_claims!(*options)
157
+ Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
158
+ @claims_verified = true
103
159
  end
160
+ rescue StandardError
161
+ @claims_verified = false
162
+ raise
163
+ end
164
+
165
+ # Returns the errors of the claims of the token.
166
+ # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
167
+ # @return [Array<Symbol>] the errors of the claims.
168
+ def claim_errors(*options)
169
+ Claims::Verifier.errors(ClaimsContext.new(self), *claims_options(options))
170
+ end
171
+
172
+ # Returns whether the claims of the token are valid.
173
+ # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
174
+ # @return [Boolean] whether the claims are valid.
175
+ def valid_claims?(*options)
176
+ claim_errors(*claims_options(options)).empty?.tap { |verified| @claims_verified = verified }
104
177
  end
105
178
 
106
179
  alias to_s jwt
107
180
 
108
181
  private
109
182
 
183
+ def claims_options(options)
184
+ return DEFAULT_CLAIMS if options.first.nil?
185
+
186
+ options
187
+ end
188
+
110
189
  def decode_payload
111
190
  raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
112
191
 
@@ -135,5 +214,9 @@ module JWT
135
214
  rescue ::JSON::ParserError
136
215
  raise JWT::DecodeError, 'Invalid segment encoding'
137
216
  end
217
+
218
+ def decoded_payload
219
+ @decoded_payload ||= decode_payload
220
+ end
138
221
  end
139
222
  end
data/lib/jwt/error.rb CHANGED
@@ -7,9 +7,6 @@ module JWT
7
7
  # The DecodeError class is raised when there is an error decoding a JWT.
8
8
  class DecodeError < StandardError; end
9
9
 
10
- # The RequiredDependencyError class is raised when a required dependency is missing.
11
- class RequiredDependencyError < StandardError; end
12
-
13
10
  # The VerificationError class is raised when there is an error verifying a JWT.
14
11
  class VerificationError < DecodeError; end
15
12
 
data/lib/jwt/jwa/ecdsa.rb CHANGED
@@ -12,14 +12,22 @@ module JWT
12
12
  end
13
13
 
14
14
  def sign(data:, signing_key:)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::EC instance") unless signing_key.is_a?(::OpenSSL::PKey::EC)
16
+ raise_sign_error!('The given key is not a private key') unless signing_key.private?
17
+
15
18
  curve_definition = curve_by_name(signing_key.group.curve_name)
16
19
  key_algorithm = curve_definition[:algorithm]
20
+
17
21
  raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
18
22
 
19
23
  asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key)
20
24
  end
21
25
 
22
26
  def verify(data:, signature:, verification_key:)
27
+ verification_key = self.class.create_public_key_from_point(verification_key) if verification_key.is_a?(::OpenSSL::PKey::EC::Point)
28
+
29
+ raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::EC instance") unless verification_key.is_a?(::OpenSSL::PKey::EC)
30
+
23
31
  curve_definition = curve_by_name(verification_key.group.curve_name)
24
32
  key_algorithm = curve_definition[:algorithm]
25
33
  raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm
@@ -56,16 +64,29 @@ module JWT
56
64
  register_algorithm(new(v[:algorithm], v[:digest]))
57
65
  end
58
66
 
59
- def self.from_algorithm(algorithm)
60
- new(algorithm, algorithm.downcase.gsub('es', 'sha'))
61
- end
62
-
67
+ # @api private
63
68
  def self.curve_by_name(name)
64
69
  NAMED_CURVES.fetch(name) do
65
70
  raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
66
71
  end
67
72
  end
68
73
 
74
+ if ::JWT.openssl_3?
75
+ def self.create_public_key_from_point(point)
76
+ sequence = OpenSSL::ASN1::Sequence([
77
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(point.group.curve_name)]),
78
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
79
+ ])
80
+ OpenSSL::PKey::EC.new(sequence.to_der)
81
+ end
82
+ else
83
+ def self.create_public_key_from_point(point)
84
+ OpenSSL::PKey::EC.new(point.group.curve_name).tap do |key|
85
+ key.public_key = point
86
+ end
87
+ end
88
+ end
89
+
69
90
  private
70
91
 
71
92
  attr_reader :digest
data/lib/jwt/jwa/hmac.rb CHANGED
@@ -6,9 +6,14 @@ module JWT
6
6
  class Hmac
7
7
  include JWT::JWA::SigningAlgorithm
8
8
 
9
- def self.from_algorithm(algorithm)
10
- new(algorithm, OpenSSL::Digest.new(algorithm.downcase.gsub('hs', 'sha')))
11
- end
9
+ # Minimum key lengths for HMAC algorithms based on RFC 7518 Section 3.2.
10
+ # Keys must be at least the size of the hash output to ensure sufficient
11
+ # entropy for the algorithm's security level.
12
+ MIN_KEY_LENGTHS = {
13
+ 'HS256' => 32,
14
+ 'HS384' => 48,
15
+ 'HS512' => 64
16
+ }.freeze
12
17
 
13
18
  def initialize(alg, digest)
14
19
  @alg = alg
@@ -16,18 +21,17 @@ module JWT
16
21
  end
17
22
 
18
23
  def sign(data:, signing_key:)
19
- signing_key ||= ''
20
- raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
24
+ ensure_valid_key!(signing_key)
25
+ validate_key_length!(signing_key)
21
26
 
22
27
  OpenSSL::HMAC.digest(digest.new, signing_key, data)
23
- rescue OpenSSL::HMACError => e
24
- raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret') if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
25
-
26
- raise e
27
28
  end
28
29
 
29
30
  def verify(data:, signature:, verification_key:)
30
- SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
31
+ ensure_valid_key!(verification_key)
32
+ validate_key_length!(verification_key)
33
+
34
+ SecurityUtils.secure_compare(signature, OpenSSL::HMAC.digest(digest.new, verification_key, data))
31
35
  end
32
36
 
33
37
  register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
@@ -38,6 +42,20 @@ module JWT
38
42
 
39
43
  attr_reader :digest
40
44
 
45
+ def ensure_valid_key!(key)
46
+ raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)
47
+ raise_verify_error!('HMAC key cannot be empty') if key.empty?
48
+ end
49
+
50
+ def validate_key_length!(key)
51
+ return unless JWT.configuration.decode.enforce_hmac_key_length
52
+
53
+ min_length = MIN_KEY_LENGTHS[alg]
54
+ return if key.bytesize >= min_length
55
+
56
+ raise_verify_error!("HMAC key must be at least #{min_length} bytes for #{alg} algorithm")
57
+ end
58
+
41
59
  # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
42
60
  # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
43
61
  module SecurityUtils
data/lib/jwt/jwa/ps.rb CHANGED
@@ -13,6 +13,7 @@ module JWT
13
13
 
14
14
  def sign(data:, signing_key:)
15
15
  raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance.") unless signing_key.is_a?(::OpenSSL::PKey::RSA)
16
+ raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
16
17
 
17
18
  signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm)
18
19
  end
data/lib/jwt/jwa/rsa.rb CHANGED
@@ -13,6 +13,7 @@ module JWT
13
13
 
14
14
  def sign(data:, signing_key:)
15
15
  raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless signing_key.is_a?(OpenSSL::PKey::RSA)
16
+ raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
16
17
 
17
18
  signing_key.sign(OpenSSL::Digest.new(digest), data)
18
19
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ # @api private
6
+ class SignerContext
7
+ attr_reader :jwa
8
+
9
+ def initialize(jwa:, key:)
10
+ @jwa = jwa
11
+ @key = key
12
+ end
13
+
14
+ def sign(*args, **kwargs)
15
+ @jwa.sign(*args, **kwargs, signing_key: @key)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -14,7 +14,6 @@ module JWT
14
14
 
15
15
  def self.included(klass)
16
16
  klass.extend(ClassMethods)
17
- klass.include(JWT::JWA::Compat)
18
17
  end
19
18
 
20
19
  attr_reader :alg