jwt 3.1.2 → 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: 4fbf6e518cee3ac505360ea356f34fab6a68c5dfc2112671105085fbb03c08df
4
- data.tar.gz: 0f206bdf51b4a979b6f734d6582f7e18762f2d3a1ee7feb38499fc4a92d77115
3
+ metadata.gz: 9b0bbac64c8b97deb791cc74f29243c6c04cbc2c3a63904484259c5379a2650f
4
+ data.tar.gz: 0252d7993ad937c9cb961d4f2ff4ce9cef9fc7b503fadb2ba4e85923752ed6b4
5
5
  SHA512:
6
- metadata.gz: 4fa9df3dae62f1abbe065fd144641a8869faddd45be94cb58871dc690fead80f38f741b5de5319b5a31d8d28fe16ae32627f27d396cbe2f91f80acc9a6d3e477
7
- data.tar.gz: 41e30090c5ee55b3706b4d2bddd73dc596e4db46669c292af56bca9ebe9f36a8eafe7ce33578f3012d87f910a6880ee26e6c26c46bfda59470a23f24e47a739d
6
+ metadata.gz: cdef3e40a9879182bd028df5dad15676280ba567839a12d3ca31f9569c9b1d44b02f959b256e1d95d1c9d3e882a3ab6d944dbcce0f22d31a0658cd5589e14917
7
+ data.tar.gz: 88f3879e2061b072b15c1a001ff7c62e674fcf61a2edd610b19f95b9aac9dfd7fd07ce2aa416a3859bcc1afb3e59f18cf12e95898027102bd16f13103d04aa26
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
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
+
21
+ ## [v3.2.0](https://github.com/jwt/ruby-jwt/tree/v3.2.0) (2026-05-13)
22
+
23
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.2...v3.2.0)
24
+
25
+ **Features:**
26
+
27
+ - Add `enforce_hmac_key_length` configuration option [#716](https://github.com/jwt/ruby-jwt/pull/716) - ([@304](https://github.com/304))
28
+
29
+ **Fixes and enhancements:**
30
+
31
+ - Reject `nil` and empty HMAC keys when signing and verifying ([CVE-2026-45363](https://www.cve.org/CVERecord?id=CVE-2026-45363) / [GHSA-c32j-vqhx-rx3x](https://github.com/jwt/ruby-jwt/security/advisories/GHSA-c32j-vqhx-rx3x))
32
+ - Fix compatibility with the openssl 4.0 gem [#706](https://github.com/jwt/ruby-jwt/pull/706)
33
+ - Test against Ruby 4.0 on CI [#707](https://github.com/jwt/ruby-jwt/pull/707)
34
+ - Fix type error when header is not a JSON object [#715](https://github.com/jwt/ruby-jwt/pull/715) - ([@304](https://github.com/304))
35
+
3
36
  ## [v3.1.2](https://github.com/jwt/ruby-jwt/tree/v3.1.2) (2025-06-28)
4
37
 
5
38
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.1...v3.1.2)
@@ -74,7 +107,7 @@ Take a look at the [upgrade guide](UPGRADING.md) for more details.
74
107
  - JWT::Token and JWT::EncodedToken for signing and verifying tokens [#621](https://github.com/jwt/ruby-jwt/pull/621) ([@anakinj](https://github.com/anakinj))
75
108
  - Detached payload support for JWT::Token and JWT::EncodedToken [#630](https://github.com/jwt/ruby-jwt/pull/630) ([@anakinj](https://github.com/anakinj))
76
109
  - Skip decoding payload if b64 header is present and false [#631](https://github.com/jwt/ruby-jwt/pull/631) ([@anakinj](https://github.com/anakinj))
77
- - Remove a few custom Rubocop configs [#638](https://github.com/jwt/ruby-jwt/pull/638) ([@anakinj](https://github.com/anakinj))
110
+ - Remove a few custom RuboCop configs [#638](https://github.com/jwt/ruby-jwt/pull/638) ([@anakinj](https://github.com/anakinj))
78
111
 
79
112
  **Fixes and enhancements:**
80
113
 
@@ -352,7 +385,7 @@ Take a look at the [upgrade guide](UPGRADING.md) for more details.
352
385
  **Implemented enhancements:**
353
386
 
354
387
  - JWK does not decode. [\#332](https://github.com/jwt/ruby-jwt/issues/332)
355
- - Inconsistent use of symbol and string keys in args \(exp and alrogithm\). [\#331](https://github.com/jwt/ruby-jwt/issues/331)
388
+ - Inconsistent use of symbol and string keys in args \(exp and algorithm\). [\#331](https://github.com/jwt/ruby-jwt/issues/331)
356
389
  - Pin simplecov to \< 0.18 [\#356](https://github.com/jwt/ruby-jwt/pull/356) ([anakinj](https://github.com/anakinj))
357
390
  - verifies algorithm before evaluating keyfinder [\#346](https://github.com/jwt/ruby-jwt/pull/346) ([jb08](https://github.com/jb08))
358
391
  - Update Rails 6 appraisal to use actual release version [\#336](https://github.com/jwt/ruby-jwt/pull/336) ([smudge](https://github.com/smudge))
@@ -467,7 +500,7 @@ Take a look at the [upgrade guide](UPGRADING.md) for more details.
467
500
  - 'DecodeError'will replace 'ExpiredSignature' [\#260](https://github.com/jwt/ruby-jwt/issues/260)
468
501
  - TypeError: no implicit conversion of OpenSSL::PKey::RSA into String [\#259](https://github.com/jwt/ruby-jwt/issues/259)
469
502
  - NameError: uninitialized constant JWT::Algos::Eddsa::RbNaCl [\#258](https://github.com/jwt/ruby-jwt/issues/258)
470
- - Get new token if curren token expired [\#256](https://github.com/jwt/ruby-jwt/issues/256)
503
+ - Get new token if current token expired [\#256](https://github.com/jwt/ruby-jwt/issues/256)
471
504
  - Infer algorithm from header [\#254](https://github.com/jwt/ruby-jwt/issues/254)
472
505
  - Why is the result of decode is an array? [\#252](https://github.com/jwt/ruby-jwt/issues/252)
473
506
  - Add support for headless token [\#251](https://github.com/jwt/ruby-jwt/issues/251)
@@ -686,8 +719,8 @@ Take a look at the [upgrade guide](UPGRADING.md) for more details.
686
719
  **Implemented enhancements:**
687
720
 
688
721
  - Refactor obsolete code for ruby 1.8 support [\#120](https://github.com/jwt/ruby-jwt/issues/120)
689
- - Fix "Rubocop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#106](https://github.com/jwt/ruby-jwt/issues/106)
690
- - Fix "Rubocop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#105](https://github.com/jwt/ruby-jwt/issues/105)
722
+ - Fix "RuboCop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#106](https://github.com/jwt/ruby-jwt/issues/106)
723
+ - Fix "RuboCop/Metrics/CyclomaticComplexity" issue in lib/jwt.rb [\#105](https://github.com/jwt/ruby-jwt/issues/105)
691
724
  - Allow a proc to be passed for JTI verification [\#126](https://github.com/jwt/ruby-jwt/pull/126) ([yahooguntu](https://github.com/yahooguntu))
692
725
  - Relax restrictions on "jti" claim verification [\#113](https://github.com/jwt/ruby-jwt/pull/113) ([lwe](https://github.com/lwe))
693
726
 
@@ -833,13 +866,13 @@ Take a look at the [upgrade guide](UPGRADING.md) for more details.
833
866
  - Signature Verification to Return Verification Error rather than decode error [\#57](https://github.com/jwt/ruby-jwt/issues/57)
834
867
  - Incorrect readme for leeway [\#55](https://github.com/jwt/ruby-jwt/issues/55)
835
868
  - What is the reason behind stripping the = in base64 encoding? [\#54](https://github.com/jwt/ruby-jwt/issues/54)
836
- - Preperations for version 2.x [\#50](https://github.com/jwt/ruby-jwt/issues/50)
869
+ - Preparations for version 2.x [\#50](https://github.com/jwt/ruby-jwt/issues/50)
837
870
  - Release a new version [\#47](https://github.com/jwt/ruby-jwt/issues/47)
838
871
  - Catch up for ActiveWhatever 4.1.1 series [\#40](https://github.com/jwt/ruby-jwt/issues/40)
839
872
 
840
873
  **Merged pull requests:**
841
874
 
842
- - raise verification error for signiture verification [\#58](https://github.com/jwt/ruby-jwt/pull/58) ([punkle](https://github.com/punkle))
875
+ - raise verification error for signature verification [\#58](https://github.com/jwt/ruby-jwt/pull/58) ([punkle](https://github.com/punkle))
843
876
  - Added support for not before claim verification [\#56](https://github.com/jwt/ruby-jwt/pull/56) ([punkle](https://github.com/punkle))
844
877
 
845
878
  ## [jwt-1.2.1](https://github.com/jwt/ruby-jwt/tree/jwt-1.2.1) (2015-01-22)
data/CONTRIBUTING.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Forking the project
4
4
 
5
- Fork the project on GitHub and clone your own fork. Instuctions on forking can be found from the [GitHub Docs](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
5
+ Fork the project on GitHub and clone your own fork. Instructions on forking can be found from the [GitHub Docs](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
6
6
 
7
7
  ```bash
8
8
  git clone git@github.com:you/ruby-jwt.git
@@ -28,7 +28,7 @@ Before you start with your implementation make sure you are able to get a succes
28
28
 
29
29
  The tests are written with rspec and [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
30
30
 
31
- [Rubocop](https://github.com/rubocop/rubocop) is used to enforce the Ruby style.
31
+ [RuboCop](https://github.com/rubocop/rubocop) is used to enforce the Ruby style.
32
32
 
33
33
  To run the complete set of tests and linter run the following
34
34
 
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
@@ -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
@@ -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 token.header.is_a?(Hash)
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,13 +109,19 @@ 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?
116
+ return false unless valid_token_header?
117
+
116
118
  alg_in_header == 'none'
117
119
  end
118
120
 
121
+ def valid_token_header?
122
+ token.header.is_a?(Hash)
123
+ end
124
+
119
125
  def alg_in_header
120
126
  token.header['alg']
121
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,22 +14,6 @@ module JWT
12
14
  # encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret')
13
15
  # encoded_token.payload # => {'pay' => 'load'}
14
16
  class EncodedToken
15
- # @private
16
- # Allow access to the unverified payload for claim verification.
17
- class ClaimsContext
18
- extend Forwardable
19
-
20
- def_delegators :@token, :header, :unverified_payload
21
-
22
- def initialize(token)
23
- @token = token
24
- end
25
-
26
- def payload
27
- unverified_payload
28
- end
29
- end
30
-
31
17
  DEFAULT_CLAIMS = [:exp].freeze
32
18
 
33
19
  private_constant(:DEFAULT_CLAIMS)
@@ -77,10 +63,10 @@ module JWT
77
63
  # Returns the payload of the JWT token. Access requires the signature and claims to have been verified.
78
64
  #
79
65
  # @return [Hash] the payload.
80
- # @raise [JWT::DecodeError] if the signature has not been verified.
66
+ # @raise [JWT::TokenError] if the signature has not been verified.
81
67
  def payload
82
- raise JWT::DecodeError, 'Verify the token signature before accessing the payload' unless @signature_verified
83
- 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
84
70
 
85
71
  decoded_payload
86
72
  end
@@ -91,10 +77,21 @@ module JWT
91
77
  decoded_payload
92
78
  end
93
79
 
94
- # Sets or returns the encoded payload of the JWT token.
80
+ # Returns the encoded payload of the JWT token.
95
81
  #
96
82
  # @return [String] the encoded payload.
97
- 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
98
95
 
99
96
  # Returns the signing input of the JWT token.
100
97
  #
@@ -112,7 +109,7 @@ module JWT
112
109
  # @param signature [Hash] the parameters for signature verification (see {#verify_signature!}).
113
110
  # @param claims [Array<Symbol>, Hash] the claims to verify (see {#verify_claims!}).
114
111
  # @return [nil]
115
- # @raise [JWT::DecodeError] if the signature or claim verification fails.
112
+ # @raise [JWT::Error] if the signature or claim verification fails.
116
113
  def verify!(signature:, claims: nil)
117
114
  verify_signature!(**signature)
118
115
  claims.is_a?(Array) ? verify_claims!(*claims) : verify_claims!(claims)
@@ -166,7 +163,8 @@ module JWT
166
163
 
167
164
  # Verifies the claims of the token.
168
165
  # @param options [Array<Symbol>, Hash] the claims to verify. By default, it checks the 'exp' claim.
169
- # @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.
170
168
  def verify_claims!(*options)
171
169
  Claims::Verifier.verify!(ClaimsContext.new(self), *claims_options(options)).tap do
172
170
  @claims_verified = true
@@ -201,11 +199,11 @@ module JWT
201
199
  end
202
200
 
203
201
  def decode_payload
204
- raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
202
+ raise JWT::MalformedTokenError, 'Encoded payload is empty' if encoded_payload == ''
205
203
 
206
204
  if unencoded_payload?
207
205
  verify_claims!(crit: ['b64'])
208
- return parse_unencoded(encoded_payload)
206
+ return parse(encoded_payload)
209
207
  end
210
208
 
211
209
  parse_and_decode(encoded_payload)
@@ -219,14 +217,10 @@ module JWT
219
217
  parse(::JWT::Base64.url_decode(segment || ''))
220
218
  end
221
219
 
222
- def parse_unencoded(segment)
223
- parse(segment)
224
- end
225
-
226
220
  def parse(segment)
227
221
  JWT::JSON.parse(segment)
228
222
  rescue ::JSON::ParserError
229
- raise JWT::DecodeError, 'Invalid segment encoding'
223
+ raise JWT::MalformedTokenError, 'Invalid segment encoding'
230
224
  end
231
225
 
232
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
@@ -6,24 +6,30 @@ module JWT
6
6
  class Hmac
7
7
  include JWT::JWA::SigningAlgorithm
8
8
 
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
17
+
9
18
  def initialize(alg, digest)
10
19
  @alg = alg
11
20
  @digest = digest
12
21
  end
13
22
 
14
23
  def sign(data:, signing_key:)
15
- signing_key ||= ''
16
- raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
24
+ validate_key!(signing_key) { |message| raise_sign_error!(message) }
17
25
 
18
26
  OpenSSL::HMAC.digest(digest.new, signing_key, data)
19
- rescue OpenSSL::HMACError => e
20
- 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'
21
-
22
- raise e
23
27
  end
24
28
 
25
29
  def verify(data:, signature:, verification_key:)
26
- SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
30
+ validate_key!(verification_key) { |message| raise_verify_error!(message) }
31
+
32
+ SecurityUtils.secure_compare(signature, OpenSSL::HMAC.digest(digest.new, verification_key, data))
27
33
  end
28
34
 
29
35
  register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
@@ -34,8 +40,20 @@ module JWT
34
40
 
35
41
  attr_reader :digest
36
42
 
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?
48
+
49
+ return unless JWT.configuration.decode.enforce_hmac_key_length
50
+
51
+ min_length = MIN_KEY_LENGTHS[alg]
52
+ yield "HMAC key must be at least #{min_length} bytes for #{alg} algorithm" if key.bytesize < min_length
53
+ end
54
+
37
55
  # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
38
- # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
56
+ # rubocop:disable-next Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
39
57
  module SecurityUtils
40
58
  # Constant time string comparison, for fixed length strings.
41
59
  #
@@ -51,7 +69,7 @@ module JWT
51
69
  def fixed_length_secure_compare(a, b)
52
70
  raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
53
71
 
54
- l = a.unpack "C#{a.bytesize}"
72
+ l = a.unpack("C#{a.bytesize}")
55
73
 
56
74
  res = 0
57
75
  b.each_byte { |byte| res |= byte ^ l.shift }
@@ -72,7 +90,6 @@ module JWT
72
90
  end
73
91
  module_function :secure_compare
74
92
  end
75
- # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
76
93
  end
77
94
  end
78
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'
@@ -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
@@ -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
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ # @api private
6
+ class VerifierContext
7
+ attr_reader :jwa
8
+
9
+ def initialize(jwa:, keys:)
10
+ @jwa = jwa
11
+ @keys = Array(keys)
12
+ end
13
+
14
+ def verify(*args, **kwargs)
15
+ @keys.any? do |key|
16
+ @jwa.verify(*args, **kwargs, verification_key: key)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/jwt/jwa.rb CHANGED
@@ -9,40 +9,12 @@ require_relative 'jwa/none'
9
9
  require_relative 'jwa/ps'
10
10
  require_relative 'jwa/rsa'
11
11
  require_relative 'jwa/unsupported'
12
+ require_relative 'jwa/verifier_context'
13
+ require_relative 'jwa/signer_context'
12
14
 
13
15
  module JWT
14
16
  # The JWA module contains all supported algorithms.
15
17
  module JWA
16
- # @api private
17
- class VerifierContext
18
- attr_reader :jwa
19
-
20
- def initialize(jwa:, keys:)
21
- @jwa = jwa
22
- @keys = Array(keys)
23
- end
24
-
25
- def verify(*args, **kwargs)
26
- @keys.any? do |key|
27
- @jwa.verify(*args, **kwargs, verification_key: key)
28
- end
29
- end
30
- end
31
-
32
- # @api private
33
- class SignerContext
34
- attr_reader :jwa
35
-
36
- def initialize(jwa:, key:)
37
- @jwa = jwa
38
- @key = key
39
- end
40
-
41
- def sign(*args, **kwargs)
42
- @jwa.sign(*args, **kwargs, signing_key: @key)
43
- end
44
- end
45
-
46
18
  class << self
47
19
  # @api private
48
20
  def resolve(algorithm)
@@ -65,7 +37,7 @@ module JWT
65
37
  # @api private
66
38
  def create_signer(algorithm:, key:)
67
39
  if key.is_a?(JWK::KeyBase)
68
- validate_jwk_algorithms!(key, algorithm, DecodeError)
40
+ validate_jwk_algorithms!(key, algorithm, EncodeError)
69
41
 
70
42
  return key
71
43
  end
@@ -77,7 +49,7 @@ module JWT
77
49
  def create_verifiers(algorithms:, keys:, preferred_algorithm:)
78
50
  jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
79
51
 
80
- validate_jwk_algorithms!(jwks, algorithms, VerificationError)
52
+ validate_jwk_algorithms!(jwks, algorithms, VerificationKeyError)
81
53
 
82
54
  jwks + resolve_and_sort(algorithms: algorithms,
83
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
@@ -195,7 +195,7 @@ module JWT
195
195
 
196
196
  if ::JWT.openssl_3?
197
197
  alias create_rsa_key create_rsa_key_using_der
198
- elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
198
+ elsif OpenSSL::PKey::RSA.method_defined?(:set_key)
199
199
  alias create_rsa_key create_rsa_key_using_sets
200
200
  else
201
201
  alias create_rsa_key create_rsa_key_using_accessors
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,8 +15,8 @@ module JWT
15
15
  # Version constants
16
16
  module VERSION
17
17
  MAJOR = 3
18
- MINOR = 1
19
- TINY = 2
18
+ MINOR = 3
19
+ TINY = 0
20
20
  PRE = nil
21
21
 
22
22
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -32,14 +32,6 @@ module JWT
32
32
  true if 3 * 0x10000000 <= OpenSSL::OPENSSL_VERSION_NUMBER
33
33
  end
34
34
 
35
- # Checks if there is an OpenSSL 3 HMAC empty key regression.
36
- #
37
- # @return [Boolean] true if there is an OpenSSL 3 HMAC empty key regression, false otherwise.
38
- # @api private
39
- def self.openssl_3_hmac_empty_key_regression?
40
- openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0')
41
- end
42
-
43
35
  # Returns the OpenSSL version.
44
36
  #
45
37
  # @return [Gem::Version] the OpenSSL version.
@@ -40,7 +40,7 @@ module JWT
40
40
  end
41
41
 
42
42
  def parse_certificates(x5c_header_or_certificates)
43
- if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) }
43
+ if x5c_header_or_certificates.all?(OpenSSL::X509::Certificate)
44
44
  x5c_header_or_certificates
45
45
  else
46
46
  x5c_header_or_certificates.map do |encoded|
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.1.2
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rudat
@@ -171,6 +171,7 @@ files:
171
171
  - lib/jwt/decode.rb
172
172
  - lib/jwt/encode.rb
173
173
  - lib/jwt/encoded_token.rb
174
+ - lib/jwt/encoded_token/claims_context.rb
174
175
  - lib/jwt/error.rb
175
176
  - lib/jwt/json.rb
176
177
  - lib/jwt/jwa.rb
@@ -179,8 +180,10 @@ files:
179
180
  - lib/jwt/jwa/none.rb
180
181
  - lib/jwt/jwa/ps.rb
181
182
  - lib/jwt/jwa/rsa.rb
183
+ - lib/jwt/jwa/signer_context.rb
182
184
  - lib/jwt/jwa/signing_algorithm.rb
183
185
  - lib/jwt/jwa/unsupported.rb
186
+ - lib/jwt/jwa/verifier_context.rb
184
187
  - lib/jwt/jwk.rb
185
188
  - lib/jwt/jwk/ec.rb
186
189
  - lib/jwt/jwk/hmac.rb
@@ -199,7 +202,7 @@ licenses:
199
202
  - MIT
200
203
  metadata:
201
204
  bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues
202
- changelog_uri: https://github.com/jwt/ruby-jwt/blob/v3.1.2/CHANGELOG.md
205
+ changelog_uri: https://github.com/jwt/ruby-jwt/blob/v3.3.0/CHANGELOG.md
203
206
  rubygems_mfa_required: 'true'
204
207
  rdoc_options: []
205
208
  require_paths:
@@ -215,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
215
218
  - !ruby/object:Gem::Version
216
219
  version: '0'
217
220
  requirements: []
218
- rubygems_version: 3.6.7
221
+ rubygems_version: 4.0.16
219
222
  specification_version: 4
220
223
  summary: JSON Web Token implementation in Ruby
221
224
  test_files: []