jwt 3.2.0 → 3.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bb396cd444a952a2c732720675f053ce5c4557978acf54acc0753a98b81f402
4
- data.tar.gz: deeb4ea2635a1620f8dab94b4b7b2d6b7a01087ea0f45b9f79b408c9a7b23c25
3
+ metadata.gz: 9b0bbac64c8b97deb791cc74f29243c6c04cbc2c3a63904484259c5379a2650f
4
+ data.tar.gz: 0252d7993ad937c9cb961d4f2ff4ce9cef9fc7b503fadb2ba4e85923752ed6b4
5
5
  SHA512:
6
- metadata.gz: d9913bf66785a88c127148c5a4a3fd6dcd9a28820f53c20635ab5469cebdfb159366566ba8b198154c9a541927224c11e75ced5c9f5b69c1d32a9e01764ca0dd
7
- data.tar.gz: e2d4765fa99b67229f3204b917aea480a1d4027c23a216f307f92d6ffa8acd049b72071377d7e0c4c8fcba5eb845f9751c944dbc87e9a597020665859edd40c1
6
+ metadata.gz: cdef3e40a9879182bd028df5dad15676280ba567839a12d3ca31f9569c9b1d44b02f959b256e1d95d1c9d3e882a3ab6d944dbcce0f22d31a0658cd5589e14917
7
+ data.tar.gz: 88f3879e2061b072b15c1a001ff7c62e674fcf61a2edd610b19f95b9aac9dfd7fd07ce2aa416a3859bcc1afb3e59f18cf12e95898027102bd16f13103d04aa26
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## [v3.3.0](https://github.com/jwt/ruby-jwt/tree/v3.3.0) (2026-09-11)
4
+
5
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.2.0...v3.3.0)
6
+
7
+ **Features:**
8
+
9
+ - Allow a leeway to be given for the `iat` claim verification [#747](https://github.com/jwt/ruby-jwt/pull/747) - ([@denis1011101](https://github.com/denis1011101))
10
+ - Revamp the error hierarchy under a new `JWT::Error` base class; signing failures now consistently raise `JWT::EncodeError`, see [UPGRADING.md](UPGRADING.md) [#722](https://github.com/jwt/ruby-jwt/pull/722) ([@anakinj](https://github.com/anakinj))
11
+
12
+ **Fixes and enhancements:**
13
+
14
+ - Refactor `JWT::JWK::Set#initialize` so each construction path is a named method [#758](https://github.com/jwt/ruby-jwt/pull/758) ([@anakinj](https://github.com/anakinj))
15
+ - Fix rejection of unknown algorithms from JWKs for RFC compliance and pquip [#728](https://github.com/jwt/ruby-jwt/pull/728)
16
+ - Fix the `Style/DirectiveScope` RuboCop offense failing the build [#752](https://github.com/jwt/ruby-jwt/pull/752)
17
+ - Fix `JWT::JWK::Set` sharing its key collection with the set it was copied from [#751](https://github.com/jwt/ruby-jwt/pull/751)
18
+ - Reset the decoded payload and verification state in `JWT::EncodedToken#encoded_payload=` [#749](https://github.com/jwt/ruby-jwt/issues/749)
19
+ - Fix `JWT::Token#detach_payload!` not invalidating an already rendered token [#748](https://github.com/jwt/ruby-jwt/issues/748)
20
+
3
21
  ## [v3.2.0](https://github.com/jwt/ruby-jwt/tree/v3.2.0) (2026-05-13)
4
22
 
5
23
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.2...v3.2.0)
data/README.md CHANGED
@@ -532,7 +532,9 @@ end
532
532
 
533
533
  From [Oauth JSON Web Token 4.1.6. "iat" (Issued At) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.6):
534
534
 
535
- > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. The `leeway` option is not taken into account when verifying this claim. The `iat_leeway` option was removed in version 2.2.0. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL.
535
+ > The `iat` (issued at) claim identifies the time at which the JWT was issued. This claim can be used to determine the age of the JWT. Its value MUST be a number containing a **_NumericDate_** value. Use of this claim is OPTIONAL.
536
+
537
+ The global `leeway` option does not apply to `iat`. To allow for clock drift, pass `leeway` under `verify_iat`, as shown below. The `iat_leeway` option was removed in version 2.2.0.
536
538
 
537
539
  ```ruby
538
540
  iat = Time.now.to_i
@@ -548,6 +550,12 @@ rescue JWT::InvalidIatError
548
550
  end
549
551
  ```
550
552
 
553
+ By default, `iat` verification allows no clock drift. To allow drift between the issuer and verifier clocks, pass a leeway value explicitly:
554
+
555
+ ```ruby
556
+ decoded_token = JWT.decode(token, hmac_secret, true, { verify_iat: { leeway: 30 }, algorithm: 'HS256' })
557
+ ```
558
+
551
559
  ### Subject Claim
552
560
 
553
561
  From [Oauth JSON Web Token 4.1.2. "sub" (Subject) Claim](https://tools.ietf.org/html/rfc7519#section-4.1.2):
@@ -632,7 +640,7 @@ end
632
640
 
633
641
  begin
634
642
  JWT.decode(token, nil, true, { x5c: { root_certificates: root_certificates, crls: crls } })
635
- rescue JWT::DecodeError
643
+ rescue JWT::TokenError
636
644
  # Handle error, e.g. x5c header certificate revoked or expired
637
645
  end
638
646
  ```
@@ -697,8 +705,8 @@ begin
697
705
  JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks_loader })
698
706
  rescue JWT::JWKError
699
707
  # Handle problems with the provided JWKs
700
- rescue JWT::DecodeError
701
- # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
708
+ rescue JWT::TokenError
709
+ # Handle other token related issues e.g. no kid in header, no matching public key found etc.
702
710
  end
703
711
  ```
704
712
 
data/UPGRADING.md CHANGED
@@ -1,3 +1,57 @@
1
+ # Upgrading ruby-jwt to >= 3.3.0
2
+
3
+ ## Error hierarchy revamp
4
+
5
+ The [error classes were reorganised](https://github.com/jwt/ruby-jwt/pull/722) under a new `JWT::Error` base class, so failures can be rescued by category instead of one class at a time:
6
+
7
+ - `JWT::Error` is the base class for everything the gem raises.
8
+ - `JWT::TokenError` covers every failure in processing a token, and splits into `JWT::MalformedTokenError` (the token is structurally invalid), `JWT::SignatureError` (signature and algorithm problems) and `JWT::ClaimValidationError` (a claim did not verify).
9
+ - `JWT::VerificationKeyError`, a subclass of `JWT::VerificationError`, says the key or algorithm given for verification cannot be used, as opposed to a signature that does not match.
10
+
11
+ ### Backwards compatibility
12
+
13
+ This is a backwards compatible change for effectively every application. The new classes were inserted above the existing ones rather than replacing them, so every error class you already rescue keeps its name, its meaning and everything it used to catch. In the ordinary case, upgrading needs no code change at all.
14
+
15
+ There is one exception, and it is narrow enough to be worth stating precisely. It applies only if all three of the following are true:
16
+
17
+ 1. You rescue around `JWT.encode`, not around `JWT.decode`.
18
+ 2. The class you rescue is `JWT::DecodeError`, `JWT::IncorrectAlgorithm`, `JWT::UnsupportedEcdsaCurve` or `ArgumentError`.
19
+ 3. That rescue is reached at all, which takes a key or algorithm that cannot sign in the first place.
20
+
21
+ If any one of the three does not hold, there is nothing to do. If all three do, the fix is a one line change, described in the next section.
22
+
23
+ Why that is a small target in practice:
24
+
25
+ - **Decoding is untouched.** `JWT::DecodeError` is deprecated in favour of the classes above, but it keeps its meaning: every error class except `JWT::EncodeError` still inherits from it. A `rescue JWT::DecodeError` around `JWT.decode` catches everything it caught before, and the specific classes it has always raised, such as `JWT::ExpiredSignature`, are unchanged.
26
+ - **Signing that currently works is untouched.** Every case in the table below is an unusable key or algorithm, a misconfiguration that fails on every call made with that key. None of them can be triggered by a particular payload or token, so an application that signs tokens successfully today does not reach them at all.
27
+ - **Failures you let escape are untouched.** All of these raised before and still raise; only the class changed. Code that does not rescue them behaves exactly as it did.
28
+
29
+ One decode-side behaviour did change, but in the direction of catching more: `RS*` and `PS*` now reject a key of the wrong type with a `JWT::VerificationKeyError`, which is a `JWT::DecodeError`, instead of letting a `NoMethodError` escape.
30
+
31
+ ### Signing failures now raise `JWT::EncodeError`
32
+
33
+ Signing failures used to surface as decode errors, and are now consistently `JWT::EncodeError`, which is deliberately not a `JWT::DecodeError`:
34
+
35
+ | Signing with | Used to raise | Now raises |
36
+ | --- | --- | --- |
37
+ | a `nil`, empty or too short HMAC key | `JWT::DecodeError` | `JWT::EncodeError` |
38
+ | an ECDSA key whose curve does not match the algorithm | `JWT::IncorrectAlgorithm` | `JWT::EncodeError` |
39
+ | an ECDSA key on an unsupported curve | `JWT::UnsupportedEcdsaCurve` | `JWT::EncodeError` |
40
+ | a JWK whose `alg` does not match the algorithm | `JWT::DecodeError` | `JWT::EncodeError` |
41
+ | an `RS*` or `PS*` public key | `ArgumentError` | `JWT::EncodeError` |
42
+
43
+ If you wrap `JWT.encode` in `rescue JWT::DecodeError`, `rescue JWT::IncorrectAlgorithm` or `rescue JWT::UnsupportedEcdsaCurve`, rescue `JWT::EncodeError` or `JWT::Error` instead.
44
+
45
+ ### Why this is a minor release
46
+
47
+ The exception described above is a real incompatibility, and 3.3.0 is still deliberately a minor release rather than a new major.
48
+
49
+ The reason is that nothing here turns a call that used to succeed into one that fails, or the other way round. Every case in the table raised an error before and raises an error now, only under a different class. No token is signed that would previously have been refused, no token verifies that would previously have been rejected, and no signature is produced or accepted on different terms than before. What changed is which `rescue` clause matches on a path that was already failing.
50
+
51
+ Weighed against the cost of a second major migration so soon after 3.0.0, that did not seem to warrant one.
52
+
53
+ If you find a case where this change affects whether a call succeeds, rather than which error it raises when it fails, please [open an issue](https://github.com/jwt/ruby-jwt/issues). That would be a bug rather than an intended consequence of the reorganisation.
54
+
1
55
  # Upgrading ruby-jwt to >= 3.0.0
2
56
 
3
57
  ## Removal of the indirect [RbNaCl](https://github.com/RubyCrypto/rbnacl) dependency
@@ -15,7 +15,7 @@ module JWT
15
15
  verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) },
16
16
  verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) },
17
17
  verify_iss: ->(options) { options[:iss] && Claims::Issuer.new(issuers: options[:iss]) },
18
- verify_iat: ->(*) { Claims::IssuedAt.new },
18
+ verify_iat: ->(options) { Claims::IssuedAt.new(leeway: options[:verify_iat].is_a?(Hash) ? options[:verify_iat][:leeway] : nil) },
19
19
  verify_jti: ->(options) { Claims::JwtId.new(validator: options[:verify_jti]) },
20
20
  verify_aud: ->(options) { options[:aud] && Claims::Audience.new(expected_audience: options[:aud]) },
21
21
  verify_sub: ->(options) { options[:sub] && Claims::Subject.new(expected_subject: options[:sub]) },
@@ -4,6 +4,13 @@ module JWT
4
4
  module Claims
5
5
  # The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token.
6
6
  class IssuedAt
7
+ # Initializes a new IssuedAt instance.
8
+ #
9
+ # @param leeway [Integer] the drift (in seconds) to allow between the clock of the issuer and the clock of the verifier. Default: 0.
10
+ def initialize(leeway: 0)
11
+ @leeway = leeway || 0
12
+ end
13
+
7
14
  # Verifies the issued at claim ('iat') in the JWT token.
8
15
  #
9
16
  # @param context [Object] the context containing the JWT payload.
@@ -15,8 +22,12 @@ module JWT
15
22
  return unless context.payload.key?('iat')
16
23
 
17
24
  iat = context.payload['iat']
18
- raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f
25
+ raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > (Time.now.to_f + leeway)
19
26
  end
27
+
28
+ private
29
+
30
+ attr_reader :leeway
20
31
  end
21
32
  end
22
33
  end
@@ -8,7 +8,7 @@ module JWT
8
8
  exp: ->(options) { Claims::Expiration.new(leeway: options.dig(:exp, :leeway)) },
9
9
  nbf: ->(options) { Claims::NotBefore.new(leeway: options.dig(:nbf, :leeway)) },
10
10
  iss: ->(options) { Claims::Issuer.new(issuers: options[:iss]) },
11
- iat: ->(*) { Claims::IssuedAt.new },
11
+ iat: ->(options) { Claims::IssuedAt.new(leeway: options.dig(:iat, :leeway)) },
12
12
  jti: ->(options) { Claims::JwtId.new(validator: options[:jti]) },
13
13
  aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) },
14
14
  sub: ->(options) { Claims::Subject.new(expected_subject: options[:sub]) },
@@ -33,8 +33,10 @@ module JWT
33
33
  errors = []
34
34
  iterate_verifiers(*options) do |verifier, verifier_options|
35
35
  verify_one!(context, verifier, verifier_options)
36
- rescue ::JWT::DecodeError => e
37
- errors << Error.new(message: e.message)
36
+ rescue ::JWT::ClaimValidationError, ::JWT::MalformedTokenError => e
37
+ # A payload that cannot be decoded has no valid claims either, so the
38
+ # predicate API reports it instead of raising.
39
+ errors << JWT::Claims::Error.new(message: e.message)
38
40
  end
39
41
  errors
40
42
  end
data/lib/jwt/claims.rb CHANGED
@@ -41,7 +41,7 @@ module JWT
41
41
  # @param payload [Hash] the JWT payload.
42
42
  # @param options [Array] the options for verifying the claims.
43
43
  # @return [void]
44
- # @raise [JWT::DecodeError] if any claim is invalid.
44
+ # @raise [JWT::ClaimValidationError] if any claim is invalid.
45
45
  def verify_payload!(payload, *options)
46
46
  Verifier.verify!(VerificationContext.new(payload: payload), *options)
47
47
  end
@@ -11,7 +11,7 @@ module JWT
11
11
  # @!attribute [rw] verify_iss
12
12
  # @return [Boolean] whether to verify the issuer claim.
13
13
  # @!attribute [rw] verify_iat
14
- # @return [Boolean] whether to verify the issued at claim.
14
+ # @return [Boolean, Hash] whether to verify the issued at claim. A hash can be given to configure the claim, currently only `leeway` is supported.
15
15
  # @!attribute [rw] verify_jti
16
16
  # @return [Boolean] whether to verify the JWT ID claim.
17
17
  # @!attribute [rw] verify_aud
data/lib/jwt/decode.rb CHANGED
@@ -18,9 +18,9 @@ module JWT
18
18
  # @param verify [Boolean] whether to verify the token's signature.
19
19
  # @param options [Hash] additional options for decoding and verification.
20
20
  # @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification.
21
- # @raise [JWT::DecodeError] if decoding or verification fails.
21
+ # @raise [JWT::Error] if decoding or verification fails.
22
22
  def initialize(jwt, key, verify, options, &keyfinder)
23
- raise JWT::DecodeError, 'Nil JSON web token' unless jwt
23
+ raise JWT::MalformedTokenError, 'Nil JSON web token' unless jwt
24
24
 
25
25
  @token = EncodedToken.new(jwt)
26
26
  @key = key
@@ -51,14 +51,14 @@ module JWT
51
51
  def verify_signature
52
52
  return if none_algorithm?
53
53
 
54
- raise JWT::DecodeError, 'No verification key available' unless @key
54
+ raise JWT::SignatureError, 'No verification key available' unless @key
55
55
 
56
56
  token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key)
57
57
  end
58
58
 
59
59
  def verify_algo
60
60
  raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
61
- raise JWT::DecodeError, 'Token header not a JSON object' unless valid_token_header?
61
+ raise JWT::MalformedTokenError, 'Token header not a JSON object' unless valid_token_header?
62
62
  raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
63
63
  raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
64
64
  end
@@ -100,7 +100,7 @@ module JWT
100
100
  # key can be of type [string, nil, OpenSSL::PKey, Array]
101
101
  return key if key && !Array(key).empty?
102
102
 
103
- raise JWT::DecodeError, 'No verification key available'
103
+ raise JWT::SignatureError, 'No verification key available'
104
104
  end
105
105
 
106
106
  def validate_segment_count!
@@ -109,7 +109,7 @@ module JWT
109
109
  return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed
110
110
  return if segment_count == 2 && none_algorithm?
111
111
 
112
- raise JWT::DecodeError, 'Not enough or too many segments'
112
+ raise JWT::MalformedTokenError, 'Not enough or too many segments'
113
113
  end
114
114
 
115
115
  def none_algorithm?
@@ -63,10 +63,10 @@ module JWT
63
63
  # Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
64
64
  #
65
65
  # @return [Hash] the payload.
66
- # @raise [JWT::DecodeError] if the signature has not been verified.
66
+ # @raise [JWT::TokenError] if the signature has not been verified.
67
67
  def 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
68
+ raise JWT::TokenError, 'Verify the token signature before accessing the payload' unless @signature_verified
69
+ raise JWT::TokenError, 'Verify the token claims before accessing the payload' unless @claims_verified
70
70
 
71
71
  decoded_payload
72
72
  end
@@ -77,10 +77,21 @@ module JWT
77
77
  decoded_payload
78
78
  end
79
79
 
80
- # Sets or returns the encoded payload of the JWT token.
80
+ # Returns the encoded payload of the JWT token.
81
81
  #
82
82
  # @return [String] the encoded payload.
83
- attr_accessor :encoded_payload
83
+ attr_reader :encoded_payload
84
+
85
+ # Sets the encoded payload of the JWT token.
86
+ #
87
+ # Resets the verification state, requiring the token to be verified again.
88
+ #
89
+ # @param encoded_payload [String] the encoded payload.
90
+ def encoded_payload=(encoded_payload)
91
+ @encoded_payload = encoded_payload
92
+ @decoded_payload = nil
93
+ @signature_verified = @claims_verified = false
94
+ end
84
95
 
85
96
  # Returns the signing input of the JWT token.
86
97
  #
@@ -98,7 +109,7 @@ module JWT
98
109
  # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
99
110
  # @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
100
111
  # @return [nil]
101
- # @raise [JWT::DecodeError] if the signature or claim verification fails.
112
+ # @raise [JWT::Error] if the signature or claim verification fails.
102
113
  def verify!(signature:, claims: nil)
103
114
  verify_signature!(**signature)
104
115
  claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims)
@@ -152,7 +163,8 @@ module JWT
152
163
 
153
164
  # Verifies the claims of the token.
154
165
  # @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.
166
+ # @raise [JWT::ClaimValidationError] if the claims are invalid.
167
+ # @raise [JWT::MalformedTokenError] if the payload cannot be decoded, which happens before any claim is validated.
156
168
  def verify_claims!(*options)
157
169
  Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
158
170
  @claims_verified = true
@@ -187,11 +199,11 @@ module JWT
187
199
  end
188
200
 
189
201
  def decode_payload
190
- raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
202
+ raise JWT::MalformedTokenError, 'Encoded payload is empty' if encoded_payload == ''
191
203
 
192
204
  if unencoded_payload?
193
205
  verify_claims!(crit: ['b64'])
194
- return parse_unencoded(encoded_payload)
206
+ return parse(encoded_payload)
195
207
  end
196
208
 
197
209
  parse_and_decode(encoded_payload)
@@ -205,14 +217,10 @@ module JWT
205
217
  parse(::JWT::Base64.url_decode(segment || ''))
206
218
  end
207
219
 
208
- def parse_unencoded(segment)
209
- parse(segment)
210
- end
211
-
212
220
  def parse(segment)
213
221
  JWT::JSON.parse(segment)
214
222
  rescue ::JSON::ParserError
215
- raise JWT::DecodeError, 'Invalid segment encoding'
223
+ raise JWT::MalformedTokenError, 'Invalid segment encoding'
216
224
  end
217
225
 
218
226
  def decoded_payload
data/lib/jwt/error.rb CHANGED
@@ -1,54 +1,81 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
+ # The base error class for all JWT errors.
5
+ class Error < StandardError; end
6
+
4
7
  # The EncodeError class is raised when there is an error encoding a JWT.
5
- class EncodeError < StandardError; end
8
+ class EncodeError < Error; end
6
9
 
7
- # The DecodeError class is raised when there is an error decoding a JWT.
8
- class DecodeError < StandardError; end
10
+ # The historical grouping of every error that is not an encoding error. Every
11
+ # error class below is a descendant, so `rescue JWT::DecodeError` keeps its
12
+ # original meaning.
13
+ #
14
+ # @deprecated Use {JWT::Error}, {JWT::TokenError} or a more specific error class instead.
15
+ class DecodeError < Error; end
9
16
 
10
- # The VerificationError class is raised when there is an error verifying a JWT.
11
- class VerificationError < DecodeError; end
17
+ # The TokenError class is the base class for all errors related to token processing.
18
+ class TokenError < DecodeError; end
12
19
 
13
- # The ExpiredSignature class is raised when the JWT signature has expired.
14
- class ExpiredSignature < DecodeError; end
20
+ # The MalformedTokenError class is raised when the token is structurally invalid.
21
+ class MalformedTokenError < TokenError; end
15
22
 
16
- # The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect.
17
- class IncorrectAlgorithm < DecodeError; end
23
+ # The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string.
24
+ class Base64DecodeError < MalformedTokenError; end
18
25
 
19
- # The ImmatureSignature class is raised when the JWT signature is immature.
20
- class ImmatureSignature < DecodeError; end
26
+ # The SignatureError class is the base class for signature and algorithm related errors.
27
+ class SignatureError < TokenError; end
21
28
 
22
- # The InvalidIssuerError class is raised when the JWT issuer is invalid.
23
- class InvalidIssuerError < DecodeError; end
29
+ # The VerificationError class is raised when the signature of a token does not
30
+ # match the one calculated from the signing input.
31
+ class VerificationError < SignatureError; end
32
+
33
+ # The VerificationKeyError class is raised when the key or algorithm given for
34
+ # verification cannot be used, as opposed to a signature that does not match.
35
+ class VerificationKeyError < VerificationError; end
36
+
37
+ # The IncorrectAlgorithm class is raised when the JWT algorithm is incorrect.
38
+ class IncorrectAlgorithm < SignatureError; end
24
39
 
25
40
  # The UnsupportedEcdsaCurve class is raised when the ECDSA curve is unsupported.
26
41
  class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
27
42
 
43
+ # The ClaimValidationError class is the base class for all claim validation errors.
44
+ class ClaimValidationError < TokenError; end
45
+
46
+ # The ExpiredSignature class is raised when the JWT token has expired.
47
+ class ExpiredSignature < ClaimValidationError; end
48
+
49
+ # The ImmatureSignature class is raised when the JWT token is not yet valid (nbf).
50
+ class ImmatureSignature < ClaimValidationError; end
51
+
52
+ # The InvalidIssuerError class is raised when the JWT issuer is invalid.
53
+ class InvalidIssuerError < ClaimValidationError; end
54
+
28
55
  # The InvalidIatError class is raised when the JWT issued at (iat) claim is invalid.
29
- class InvalidIatError < DecodeError; end
56
+ class InvalidIatError < ClaimValidationError; end
30
57
 
31
58
  # The InvalidAudError class is raised when the JWT audience (aud) claim is invalid.
32
- class InvalidAudError < DecodeError; end
59
+ class InvalidAudError < ClaimValidationError; end
33
60
 
34
61
  # The InvalidSubError class is raised when the JWT subject (sub) claim is invalid.
35
- class InvalidSubError < DecodeError; end
62
+ class InvalidSubError < ClaimValidationError; end
36
63
 
37
64
  # The InvalidCritError class is raised when the JWT crit header is invalid.
38
- class InvalidCritError < DecodeError; end
65
+ class InvalidCritError < ClaimValidationError; end
39
66
 
40
67
  # The InvalidJtiError class is raised when the JWT ID (jti) claim is invalid.
41
- class InvalidJtiError < DecodeError; end
68
+ class InvalidJtiError < ClaimValidationError; end
42
69
 
43
70
  # The InvalidPayload class is raised when the JWT payload is invalid.
44
- class InvalidPayload < DecodeError; end
71
+ class InvalidPayload < ClaimValidationError; end
45
72
 
46
73
  # The MissingRequiredClaim class is raised when a required claim is missing from the JWT.
47
- class MissingRequiredClaim < DecodeError; end
48
-
49
- # The Base64DecodeError class is raised when there is an error decoding a Base64-encoded string.
50
- class Base64DecodeError < DecodeError; end
74
+ class MissingRequiredClaim < ClaimValidationError; end
51
75
 
52
76
  # The JWKError class is raised when there is an error with the JSON Web Key (JWK).
53
77
  class JWKError < DecodeError; end
78
+
79
+ # Raised when a JWK uses a key type (kty) that this library does not support.
80
+ class UnsupportedKeyType < JWKError; end
54
81
  end
data/lib/jwt/jwa/ecdsa.rb CHANGED
@@ -15,10 +15,8 @@ module JWT
15
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
16
  raise_sign_error!('The given key is not a private key') unless signing_key.private?
17
17
 
18
- curve_definition = curve_by_name(signing_key.group.curve_name)
19
- key_algorithm = curve_definition[:algorithm]
20
-
21
- raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
18
+ key_algorithm = signing_key_algorithm(signing_key)
19
+ raise_sign_error!("payload algorithm is #{alg} but #{key_algorithm} signing key was provided") if alg != key_algorithm
22
20
 
23
21
  asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key)
24
22
  end
@@ -95,6 +93,13 @@ module JWT
95
93
  self.class.curve_by_name(name)
96
94
  end
97
95
 
96
+ # Signing-side counterpart of {.curve_by_name}. An unsupported curve on the
97
+ # signing key is an encoding problem, so it raises a JWT::EncodeError.
98
+ def signing_key_algorithm(signing_key)
99
+ curve_name = signing_key.group.curve_name
100
+ NAMED_CURVES.fetch(curve_name) { raise_sign_error!("The ECDSA curve '#{curve_name}' is not supported") }[:algorithm]
101
+ end
102
+
98
103
  def raw_to_asn1(signature, private_key)
99
104
  byte_size = (private_key.group.degree + 7) / 8
100
105
  sig_bytes = signature[0..(byte_size - 1)]
data/lib/jwt/jwa/hmac.rb CHANGED
@@ -21,15 +21,13 @@ module JWT
21
21
  end
22
22
 
23
23
  def sign(data:, signing_key:)
24
- ensure_valid_key!(signing_key)
25
- validate_key_length!(signing_key)
24
+ validate_key!(signing_key) { |message| raise_sign_error!(message) }
26
25
 
27
26
  OpenSSL::HMAC.digest(digest.new, signing_key, data)
28
27
  end
29
28
 
30
29
  def verify(data:, signature:, verification_key:)
31
- ensure_valid_key!(verification_key)
32
- validate_key_length!(verification_key)
30
+ validate_key!(verification_key) { |message| raise_verify_error!(message) }
33
31
 
34
32
  SecurityUtils.secure_compare(signature, OpenSSL::HMAC.digest(digest.new, verification_key, data))
35
33
  end
@@ -42,22 +40,20 @@ module JWT
42
40
 
43
41
  attr_reader :digest
44
42
 
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
43
+ # Yields a message for the first problem found with the key. The caller
44
+ # raises it, so signing and verification failures keep their own error class.
45
+ def validate_key!(key)
46
+ yield 'HMAC key expected to be a String' unless key.is_a?(String)
47
+ yield 'HMAC key cannot be empty' if key.empty?
49
48
 
50
- def validate_key_length!(key)
51
49
  return unless JWT.configuration.decode.enforce_hmac_key_length
52
50
 
53
51
  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")
52
+ yield "HMAC key must be at least #{min_length} bytes for #{alg} algorithm" if key.bytesize < min_length
57
53
  end
58
54
 
59
55
  # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
60
- # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
56
+ # rubocop:disable-next Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
61
57
  module SecurityUtils
62
58
  # Constant time string comparison, for fixed length strings.
63
59
  #
@@ -73,7 +69,7 @@ module JWT
73
69
  def fixed_length_secure_compare(a, b)
74
70
  raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
75
71
 
76
- l = a.unpack "C#{a.bytesize}"
72
+ l = a.unpack("C#{a.bytesize}")
77
73
 
78
74
  res = 0
79
75
  b.each_byte { |byte| res |= byte ^ l.shift }
@@ -94,7 +90,6 @@ module JWT
94
90
  end
95
91
  module_function :secure_compare
96
92
  end
97
- # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
98
93
  end
99
94
  end
100
95
  end
data/lib/jwt/jwa/ps.rb CHANGED
@@ -13,12 +13,15 @@ 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 given key is not a private key') unless signing_key.private?
16
17
  raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
17
18
 
18
19
  signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm)
19
20
  end
20
21
 
21
22
  def verify(data:, signature:, verification_key:)
23
+ raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless verification_key.is_a?(::OpenSSL::PKey::RSA)
24
+
22
25
  verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm)
23
26
  rescue OpenSSL::PKey::PKeyError
24
27
  raise JWT::VerificationError, 'Signature verification raised'
data/lib/jwt/jwa/rsa.rb CHANGED
@@ -13,12 +13,15 @@ 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 given key is not a private key') unless signing_key.private?
16
17
  raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
17
18
 
18
19
  signing_key.sign(OpenSSL::Digest.new(digest), data)
19
20
  end
20
21
 
21
22
  def verify(data:, signature:, verification_key:)
23
+ raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless verification_key.is_a?(::OpenSSL::PKey::RSA)
24
+
22
25
  verification_key.verify(OpenSSL::Digest.new(digest), signature, data)
23
26
  rescue OpenSSL::PKey::PKeyError
24
27
  raise JWT::VerificationError, 'Signature verification raised'
@@ -35,7 +35,7 @@ module JWT
35
35
  end
36
36
 
37
37
  def raise_verify_error!(message)
38
- raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
38
+ raise(VerificationKeyError.new(message).tap { |e| e.set_backtrace(caller(1)) })
39
39
  end
40
40
 
41
41
  def raise_sign_error!(message)
@@ -12,7 +12,7 @@ module JWT
12
12
  end
13
13
 
14
14
  def verify(*)
15
- raise JWT::VerificationError, 'Algorithm not supported'
15
+ raise_verify_error!('Algorithm not supported')
16
16
  end
17
17
  end
18
18
  end
data/lib/jwt/jwa.rb CHANGED
@@ -37,7 +37,7 @@ module JWT
37
37
  # @api private
38
38
  def create_signer(algorithm:, key:)
39
39
  if key.is_a?(JWK::KeyBase)
40
- validate_jwk_algorithms!(key, algorithm, DecodeError)
40
+ validate_jwk_algorithms!(key, algorithm, EncodeError)
41
41
 
42
42
  return key
43
43
  end
@@ -49,7 +49,7 @@ module JWT
49
49
  def create_verifiers(algorithms:, keys:, preferred_algorithm:)
50
50
  jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
51
51
 
52
- validate_jwk_algorithms!(jwks, algorithms, VerificationError)
52
+ validate_jwk_algorithms!(jwks, algorithms, VerificationKeyError)
53
53
 
54
54
  jwks + resolve_and_sort(algorithms: algorithms,
55
55
  preferred_algorithm: preferred_algorithm)
data/lib/jwt/jwk/ec.rb CHANGED
@@ -54,7 +54,7 @@ module JWT
54
54
 
55
55
  def export(options = {})
56
56
  exported = parameters.clone
57
- exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
57
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
58
58
  exported
59
59
  end
60
60
 
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -47,7 +47,7 @@ module JWT
47
47
  # See https://tools.ietf.org/html/rfc7517#appendix-A.3
48
48
  def export(options = {})
49
49
  exported = parameters.clone
50
- exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
50
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
51
51
  exported
52
52
  end
53
53
 
@@ -28,12 +28,12 @@ module JWT
28
28
  # Returns the verification key for the given kid
29
29
  # @param [String] kid the key id
30
30
  def key_for(kid, key_field = :kid)
31
- raise ::JWT::DecodeError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String)
31
+ raise ::JWT::MalformedTokenError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String)
32
32
 
33
33
  jwk = resolve_key(kid, key_field)
34
34
 
35
- raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
36
- raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
35
+ raise ::JWT::SignatureError, 'No keys found in jwks' unless @jwks.any?
36
+ raise ::JWT::SignatureError, "Could not find public key for kid #{kid}" unless jwk
37
37
 
38
38
  jwk.verify_key
39
39
  end
@@ -47,7 +47,7 @@ module JWT
47
47
  return key_for(field_value, key_field) if field_value
48
48
  end
49
49
 
50
- raise ::JWT::DecodeError, 'No key id (kid) or x5t found from token headers' unless @allow_nil_kid
50
+ raise ::JWT::SignatureError, 'No key id (kid) or x5t found from token headers' unless @allow_nil_kid
51
51
 
52
52
  kid = token.header['kid']
53
53
  key_for(kid)
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -50,7 +50,7 @@ module JWT
50
50
 
51
51
  def export(options = {})
52
52
  exported = parameters.clone
53
- exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
53
+ exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
54
54
 
55
55
  exported
56
56
  end
data/lib/jwt/jwk/set.rb CHANGED
@@ -12,24 +12,24 @@ module JWT
12
12
 
13
13
  attr_reader :keys
14
14
 
15
- def initialize(jwks = nil, options = {}) # rubocop:disable Metrics/CyclomaticComplexity
16
- jwks ||= {}
17
-
15
+ def initialize(jwks = nil, options = {})
18
16
  @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'
17
+ when nil then []
18
+ when JWT::JWK::Set then jwks.keys.dup
19
+ when JWT::JWK::KeyBase then [jwks]
20
+ when Hash then build_supported_keys(jwks.transform_keys(&:to_sym)[:keys], options)
21
+ when Array then build_keys(jwks, options)
22
+ else raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK'
30
23
  end
31
24
  end
32
25
 
26
+ # Ensures a duplicated set owns its key collection. The keys themselves are
27
+ # intentionally shared; only the collection is copied.
28
+ def initialize_copy(other)
29
+ super
30
+ @keys = @keys.dup
31
+ end
32
+
33
33
  def export(options = {})
34
34
  { keys: @keys.map { |k| k.export(options) } }
35
35
  end
@@ -77,6 +77,20 @@ module JWT
77
77
  alias | union
78
78
  alias + union
79
79
  alias << add
80
+
81
+ private
82
+
83
+ def build_keys(keys, options)
84
+ [*keys].map { |key| JWT::JWK.new(key, nil, options) }
85
+ end
86
+
87
+ def build_supported_keys(keys, options)
88
+ [*keys].each_with_object([]) do |key, arr|
89
+ arr << JWT::JWK.new(key, nil, options)
90
+ rescue JWT::UnsupportedKeyType
91
+ nil
92
+ end
93
+ end
80
94
  end
81
95
  end
82
96
  end
data/lib/jwt/jwk.rb CHANGED
@@ -13,7 +13,7 @@ module JWT
13
13
  raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
14
14
 
15
15
  return mappings.fetch(jwk_kty.to_s) do |kty|
16
- raise JWT::JWKError, "Key type #{kty} not supported"
16
+ raise JWT::UnsupportedKeyType, "Key type #{kty} not supported"
17
17
  end.new(key, params, options)
18
18
  end
19
19
 
data/lib/jwt/token.rb CHANGED
@@ -81,6 +81,7 @@ module JWT
81
81
  #
82
82
  def detach_payload!
83
83
  @detached_payload = true
84
+ @jwt = nil
84
85
 
85
86
  nil
86
87
  end
@@ -104,7 +105,7 @@ module JWT
104
105
 
105
106
  # Verifies the claims of the token.
106
107
  # @param options [Array<Symbol>, Hash] the claims to verify.
107
- # @raise [JWT::DecodeError] if the claims are invalid.
108
+ # @raise [JWT::ClaimValidationError] if the claims are invalid.
108
109
  def verify_claims!(*options)
109
110
  Claims::Verifier.verify!(self, *options)
110
111
  end
data/lib/jwt/version.rb CHANGED
@@ -15,7 +15,7 @@ module JWT
15
15
  # Version constants
16
16
  module VERSION
17
17
  MAJOR = 3
18
- MINOR = 2
18
+ MINOR = 3
19
19
  TINY = 0
20
20
  PRE = nil
21
21
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rudat
@@ -202,7 +202,7 @@ licenses:
202
202
  - MIT
203
203
  metadata:
204
204
  bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues
205
- changelog_uri: https://github.com/jwt/ruby-jwt/blob/v3.2.0/CHANGELOG.md
205
+ changelog_uri: https://github.com/jwt/ruby-jwt/blob/v3.3.0/CHANGELOG.md
206
206
  rubygems_mfa_required: 'true'
207
207
  rdoc_options: []
208
208
  require_paths:
@@ -218,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
218
  - !ruby/object:Gem::Version
219
219
  version: '0'
220
220
  requirements: []
221
- rubygems_version: 4.0.10
221
+ rubygems_version: 4.0.16
222
222
  specification_version: 4
223
223
  summary: JSON Web Token implementation in Ruby
224
224
  test_files: []