jwt 2.8.2 → 2.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +38 -12
- data/lib/jwt/claims/audience.rb +20 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +22 -0
- data/lib/jwt/claims/issued_at.rb +15 -0
- data/lib/jwt/claims/issuer.rb +24 -0
- data/lib/jwt/claims/jwt_id.rb +25 -0
- data/lib/jwt/claims/not_before.rb +22 -0
- data/lib/jwt/claims/numeric.rb +55 -0
- data/lib/jwt/claims/required.rb +23 -0
- data/lib/jwt/claims/subject.rb +20 -0
- data/lib/jwt/claims/verifier.rb +62 -0
- data/lib/jwt/claims.rb +82 -0
- data/lib/jwt/claims_validator.rb +2 -23
- data/lib/jwt/decode.rb +3 -5
- data/lib/jwt/encode.rb +3 -7
- data/lib/jwt/jwa/compat.rb +29 -0
- data/lib/jwt/jwa/ecdsa.rb +42 -25
- data/lib/jwt/jwa/eddsa.rb +19 -27
- data/lib/jwt/jwa/hmac.rb +25 -17
- data/lib/jwt/jwa/hmac_rbnacl.rb +42 -43
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +39 -39
- data/lib/jwt/jwa/none.rb +7 -3
- data/lib/jwt/jwa/ps.rb +20 -14
- data/lib/jwt/jwa/rsa.rb +20 -9
- data/lib/jwt/jwa/signing_algorithm.rb +60 -0
- data/lib/jwt/jwa/unsupported.rb +8 -8
- data/lib/jwt/jwa/wrapper.rb +26 -9
- data/lib/jwt/jwa.rb +24 -36
- data/lib/jwt/verify.rb +10 -93
- data/lib/jwt/version.rb +2 -2
- data/lib/jwt.rb +4 -0
- metadata +21 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce035519b0df530866af63d7dd6cb267490700e9458fc9c32fb0bb15be446f6e
|
4
|
+
data.tar.gz: 94fd6a38f0e3c91407b04aa254d568e113877e7dcdcb4b923c28b6a320587119
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aaf541769af85436646e45a61e0f3a57d75a88e640f4a811eff35d8be61285dc77b1741e79147cf435fed5b51519863fc027722d43805c3457a159d8e235f568
|
7
|
+
data.tar.gz: f548c7bcfac5c31520ba0a1cc1b5dd546cf600277667109e7c33fde167138a3a29a930cf127cc51a7546e5b4c14e5e093a8279949107cd0d0062d016067a1723
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,71 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## Upcoming breaking changes
|
4
|
+
|
5
|
+
Notable changes in the upcoming **version 3.0**:
|
6
|
+
|
7
|
+
- The indirect dependency to [rbnacl](https://github.com/RubyCrypto/rbnacl) will be removed:
|
8
|
+
- Support for the nonstandard SHA512256 algorithm will be removed.
|
9
|
+
- Support for Ed25519 will be moved to a [separate gem](https://github.com/anakinj/jwt-eddsa) for better dependency handling.
|
10
|
+
|
11
|
+
- Base64 decoding will no longer fallback on the looser RFC 2045.
|
12
|
+
|
13
|
+
- Claim verification has been [split into separate classes](https://github.com/jwt/ruby-jwt/pull/605) and has [a new api](https://github.com/jwt/ruby-jwt/pull/626) and lead to the following deprecations:
|
14
|
+
- The `::JWT::ClaimsValidator` class will be removed in favor of the functionality provided by `::JWT::Claims`.
|
15
|
+
- The `::JWT::Claims::verify!` method will be removed in favor of `::JWT::Claims::verify_payload!`.
|
16
|
+
- The `::JWT::JWA.create` method will be removed. No recommended alternatives.
|
17
|
+
- The `::JWT::Verify` class will be removed in favor of the functionality provided by `::JWT::Claims`.
|
18
|
+
- Calling `::JWT::Claims::Numeric.new` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
|
19
|
+
- Calling `::JWT::Claims::Numeric.verify!` with a payload will be removed in favor of `::JWT::Claims::verify_payload!(payload, :numeric)`.
|
20
|
+
|
21
|
+
- The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes lead to a few deprecations and new requirements:
|
22
|
+
- The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed.
|
23
|
+
- Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module.
|
24
|
+
|
25
|
+
## [v2.9.3](https://github.com/jwt/ruby-jwt/tree/v2.9.3) (2024-10-03)
|
26
|
+
|
27
|
+
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.2...v2.9.3)
|
28
|
+
|
29
|
+
**Fixes and enhancements:**
|
30
|
+
|
31
|
+
- Return truthy value for `::JWT::ClaimsValidator#validate!` and `::JWT::Verify.verify_claims` [#628](https://github.com/jwt/ruby-jwt/pull/628) ([@anakinj](https://github.com/anakinj))
|
32
|
+
|
33
|
+
## [v2.9.2](https://github.com/jwt/ruby-jwt/tree/v2.9.2) (2024-10-03)
|
34
|
+
|
35
|
+
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.1...v2.9.2)
|
36
|
+
|
37
|
+
**Features:**
|
38
|
+
|
39
|
+
- Standalone claim verification interface [#626](https://github.com/jwt/ruby-jwt/pull/626) ([@anakinj](https://github.com/anakinj))
|
40
|
+
|
41
|
+
**Fixes and enhancements:**
|
42
|
+
|
43
|
+
- Updated README to correctly document `OpenSSL::HMAC` documentation [#617](https://github.com/jwt/ruby-jwt/pull/617) ([@aedryan](https://github.com/aedryan))
|
44
|
+
- Verify JWT header format [#622](https://github.com/jwt/ruby-jwt/pull/622) ([@304](https://github.com/304))
|
45
|
+
- Bring back `::JWT::ClaimsValidator`, `::JWT::Verify` and a few other removed interfaces for preserved backwards compatibility [#624](https://github.com/jwt/ruby-jwt/pull/624) ([@anakinj](https://github.com/anakinj))
|
46
|
+
|
47
|
+
## [v2.9.1](https://github.com/jwt/ruby-jwt/tree/v2.9.1) (2024-09-23)
|
48
|
+
|
49
|
+
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.9.0...v2.9.1)
|
50
|
+
|
51
|
+
**Fixes and enhancements:**
|
52
|
+
|
53
|
+
- Fix regression in `iss` and `aud` claim validation [#619](https://github.com/jwt/ruby-jwt/pull/619) ([@anakinj](https://github.com/anakinj))
|
54
|
+
|
55
|
+
## [v2.9.0](https://github.com/jwt/ruby-jwt/tree/v2.9.0) (2024-09-15)
|
56
|
+
|
57
|
+
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.8.2...v2.9.0)
|
58
|
+
|
59
|
+
**Features:**
|
60
|
+
|
61
|
+
- Build and push gem using a GH action [#612](https://github.com/jwt/ruby-jwt/pull/612) ([@anakinj](https://github.com/anakinj))
|
62
|
+
|
63
|
+
**Fixes and enhancements:**
|
64
|
+
|
65
|
+
- Refactor claim validators into their own classes [#605](https://github.com/jwt/ruby-jwt/pull/605) ([@anakinj](https://github.com/anakinj), [@MatteoPierro](https://github.com/MatteoPierro))
|
66
|
+
- Allow extending available algorithms [#607](https://github.com/jwt/ruby-jwt/pull/607) ([@anakinj](https://github.com/anakinj))
|
67
|
+
- Do not include the EdDSA algorithm if rbnacl not available [#613](https://github.com/jwt/ruby-jwt/pull/613) ([@anakinj](https://github.com/anakinj))
|
68
|
+
|
3
69
|
## [v2.8.2](https://github.com/jwt/ruby-jwt/tree/v2.8.2) (2024-06-18)
|
4
70
|
|
5
71
|
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.8.1...v2.8.2)
|
data/README.md
CHANGED
@@ -223,20 +223,24 @@ puts decoded_token
|
|
223
223
|
|
224
224
|
### **Custom algorithms**
|
225
225
|
|
226
|
-
|
226
|
+
When encoding or decoding a token, you can pass in a custom object through the `algorithm` option to handle signing or verification. This custom object must include or extend the `JWT::JWA::SigningAlgorithm` module and implement certain methods:
|
227
|
+
|
228
|
+
- For decoding/verifying: The object must implement the methods `alg` and `verify`.
|
229
|
+
- For encoding/signing: The object must implement the methods `alg` and `sign`.
|
230
|
+
|
231
|
+
For customization options check the details from `JWT::JWA::SigningAlgorithm`.
|
232
|
+
|
227
233
|
|
228
234
|
```ruby
|
229
235
|
module CustomHS512Algorithm
|
236
|
+
extend JWT::JWA::SigningAlgorithm
|
237
|
+
|
230
238
|
def self.alg
|
231
239
|
'HS512'
|
232
240
|
end
|
233
241
|
|
234
|
-
def self.valid_alg?(alg_to_validate)
|
235
|
-
alg_to_validate == alg
|
236
|
-
end
|
237
|
-
|
238
242
|
def self.sign(data:, signing_key:)
|
239
|
-
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'),
|
243
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha512'), signing_key, data)
|
240
244
|
end
|
241
245
|
|
242
246
|
def self.verify(data:, signature:, verification_key:)
|
@@ -526,6 +530,24 @@ rescue JWT::InvalidSubError
|
|
526
530
|
end
|
527
531
|
```
|
528
532
|
|
533
|
+
### Standalone claim verification
|
534
|
+
|
535
|
+
The JWT claim verifications can be used to verify any Hash to include expected keys and values.
|
536
|
+
|
537
|
+
A few example on verifying the claims for a payload:
|
538
|
+
```ruby
|
539
|
+
JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :numeric, :exp)
|
540
|
+
JWT::Claims.valid_payload?({"exp" => Time.now.to_i + 10}, :exp)
|
541
|
+
# => true
|
542
|
+
JWT::Claims.payload_errors({"exp" => Time.now.to_i - 10}, :exp)
|
543
|
+
# => [#<struct JWT::Claims::Error message="Signature has expired">]
|
544
|
+
JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11})
|
545
|
+
|
546
|
+
JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10, "sub" => "subject"}, :exp, sub: "subject")
|
547
|
+
```
|
548
|
+
|
549
|
+
|
550
|
+
|
529
551
|
### Finding a Key
|
530
552
|
|
531
553
|
To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT.
|
@@ -690,21 +712,25 @@ jwk_hash = jwk.export
|
|
690
712
|
thumbprint_as_the_kid = jwk_hash[:kid]
|
691
713
|
```
|
692
714
|
|
693
|
-
# Development and
|
715
|
+
# Development and testing
|
694
716
|
|
695
|
-
|
717
|
+
The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
|
696
718
|
|
697
719
|
```bash
|
698
|
-
|
720
|
+
bundle install
|
721
|
+
bundle exec appraisal rake test
|
699
722
|
```
|
700
723
|
|
701
|
-
|
724
|
+
# Releasing
|
725
|
+
|
726
|
+
To cut a new release adjust the [version.rb](lib/jwt/version.rb) and [CHANGELOG](CHANGELOG.md) with desired version numbers and dates and commit the changes. Tag the release with the version number using the following command:
|
702
727
|
|
703
728
|
```bash
|
704
|
-
|
705
|
-
bundle exec appraisal rake test
|
729
|
+
rake release:source_control_push
|
706
730
|
```
|
707
731
|
|
732
|
+
This will tag a new version an trigger a [GitHub action](.github/workflows/push_gem.yml) that eventually will push the gem to rubygems.org.
|
733
|
+
|
708
734
|
## How to contribute
|
709
735
|
See [CONTRIBUTING](CONTRIBUTING.md).
|
710
736
|
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class Audience
|
6
|
+
def initialize(expected_audience:)
|
7
|
+
@expected_audience = expected_audience
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
aud = context.payload['aud']
|
12
|
+
raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :expected_audience
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# Context class to contain the data passed to individual claim validators
|
6
|
+
#
|
7
|
+
# @private
|
8
|
+
VerificationContext = Struct.new(:payload, keyword_init: true)
|
9
|
+
|
10
|
+
# Verifiers to support the ::JWT.decode method
|
11
|
+
#
|
12
|
+
# @private
|
13
|
+
module DecodeVerifier
|
14
|
+
VERIFIERS = {
|
15
|
+
verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) },
|
16
|
+
verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) },
|
17
|
+
verify_iss: ->(options) { options[:iss] && Claims::Issuer.new(issuers: options[:iss]) },
|
18
|
+
verify_iat: ->(*) { Claims::IssuedAt.new },
|
19
|
+
verify_jti: ->(options) { Claims::JwtId.new(validator: options[:verify_jti]) },
|
20
|
+
verify_aud: ->(options) { options[:aud] && Claims::Audience.new(expected_audience: options[:aud]) },
|
21
|
+
verify_sub: ->(options) { options[:sub] && Claims::Subject.new(expected_subject: options[:sub]) },
|
22
|
+
required_claims: ->(options) { Claims::Required.new(required_claims: options[:required_claims]) }
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
private_constant(:VERIFIERS)
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# @private
|
29
|
+
def verify!(payload, options)
|
30
|
+
VERIFIERS.each do |key, verifier_builder|
|
31
|
+
next unless options[key] || options[key.to_s]
|
32
|
+
|
33
|
+
verifier_builder&.call(options)&.verify!(context: VerificationContext.new(payload: payload))
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class Expiration
|
6
|
+
def initialize(leeway:)
|
7
|
+
@leeway = leeway || 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
return unless context.payload.is_a?(Hash)
|
12
|
+
return unless context.payload.key?('exp')
|
13
|
+
|
14
|
+
raise JWT::ExpiredSignature, 'Signature has expired' if context.payload['exp'].to_i <= (Time.now.to_i - leeway)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :leeway
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class IssuedAt
|
6
|
+
def verify!(context:, **_args)
|
7
|
+
return unless context.payload.is_a?(Hash)
|
8
|
+
return unless context.payload.key?('iat')
|
9
|
+
|
10
|
+
iat = context.payload['iat']
|
11
|
+
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class Issuer
|
6
|
+
def initialize(issuers:)
|
7
|
+
@issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
case (iss = context.payload['iss'])
|
12
|
+
when *issuers
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
raise JWT::InvalidIssuerError, "Invalid issuer. Expected #{issuers}, received #{iss || '<none>'}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :issuers
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class JwtId
|
6
|
+
def initialize(validator:)
|
7
|
+
@validator = validator
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
jti = context.payload['jti']
|
12
|
+
if validator.respond_to?(:call)
|
13
|
+
verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti)
|
14
|
+
raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
|
15
|
+
elsif jti.to_s.strip.empty?
|
16
|
+
raise(JWT::InvalidJtiError, 'Missing jti')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :validator
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class NotBefore
|
6
|
+
def initialize(leeway:)
|
7
|
+
@leeway = leeway || 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
return unless context.payload.is_a?(Hash)
|
12
|
+
return unless context.payload.key?('nbf')
|
13
|
+
|
14
|
+
raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if context.payload['nbf'].to_i > (Time.now.to_i + leeway)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :leeway
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class Numeric
|
6
|
+
class Compat
|
7
|
+
def initialize(payload)
|
8
|
+
@payload = payload
|
9
|
+
end
|
10
|
+
|
11
|
+
def verify!
|
12
|
+
JWT::Claims.verify_payload!(@payload, :numeric)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
NUMERIC_CLAIMS = %i[
|
17
|
+
exp
|
18
|
+
iat
|
19
|
+
nbf
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
def self.new(*args)
|
23
|
+
return super if args.empty?
|
24
|
+
|
25
|
+
Compat.new(*args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def verify!(context:)
|
29
|
+
validate_numeric_claims(context.payload)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.verify!(payload:, **_args)
|
33
|
+
JWT::Claims.verify_payload!(payload, :numeric)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def validate_numeric_claims(payload)
|
39
|
+
NUMERIC_CLAIMS.each do |claim|
|
40
|
+
validate_is_numeric(payload, claim)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_is_numeric(payload, claim)
|
45
|
+
return unless payload.is_a?(Hash)
|
46
|
+
return unless payload.key?(claim) ||
|
47
|
+
payload.key?(claim.to_s)
|
48
|
+
|
49
|
+
return if payload[claim].is_a?(::Numeric) || payload[claim.to_s].is_a?(::Numeric)
|
50
|
+
|
51
|
+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{(payload[claim] || payload[claim.to_s]).class}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class Required
|
6
|
+
def initialize(required_claims:)
|
7
|
+
@required_claims = required_claims
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
required_claims.each do |required_claim|
|
12
|
+
next if context.payload.is_a?(Hash) && context.payload.key?(required_claim)
|
13
|
+
|
14
|
+
raise JWT::MissingRequiredClaim, "Missing required claim #{required_claim}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :required_claims
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
class Subject
|
6
|
+
def initialize(expected_subject:)
|
7
|
+
@expected_subject = expected_subject.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify!(context:, **_args)
|
11
|
+
sub = context.payload['sub']
|
12
|
+
raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || '<none>'}") unless sub.to_s == expected_subject
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_reader :expected_subject
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# @private
|
6
|
+
module Verifier
|
7
|
+
VERIFIERS = {
|
8
|
+
exp: ->(options) { Claims::Expiration.new(leeway: options.dig(:exp, :leeway)) },
|
9
|
+
nbf: ->(options) { Claims::NotBefore.new(leeway: options.dig(:nbf, :leeway)) },
|
10
|
+
iss: ->(options) { Claims::Issuer.new(issuers: options[:iss]) },
|
11
|
+
iat: ->(*) { Claims::IssuedAt.new },
|
12
|
+
jti: ->(options) { Claims::JwtId.new(validator: options[:jti]) },
|
13
|
+
aud: ->(options) { Claims::Audience.new(expected_audience: options[:aud]) },
|
14
|
+
sub: ->(options) { Claims::Subject.new(expected_subject: options[:sub]) },
|
15
|
+
|
16
|
+
required: ->(options) { Claims::Required.new(required_claims: options[:required]) },
|
17
|
+
numeric: ->(*) { Claims::Numeric.new }
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
private_constant(:VERIFIERS)
|
21
|
+
|
22
|
+
class << self
|
23
|
+
# @private
|
24
|
+
def verify!(context, *options)
|
25
|
+
iterate_verifiers(*options) do |verifier, verifier_options|
|
26
|
+
verify_one!(context, verifier, verifier_options)
|
27
|
+
end
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# @private
|
32
|
+
def errors(context, *options)
|
33
|
+
errors = []
|
34
|
+
iterate_verifiers(*options) do |verifier, verifier_options|
|
35
|
+
verify_one!(context, verifier, verifier_options)
|
36
|
+
rescue ::JWT::DecodeError => e
|
37
|
+
errors << Error.new(message: e.message)
|
38
|
+
end
|
39
|
+
errors
|
40
|
+
end
|
41
|
+
|
42
|
+
# @private
|
43
|
+
def iterate_verifiers(*options)
|
44
|
+
options.each do |element|
|
45
|
+
if element.is_a?(Hash)
|
46
|
+
element.each_key { |key| yield(key, element) }
|
47
|
+
else
|
48
|
+
yield(element, {})
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def verify_one!(context, verifier, options)
|
56
|
+
verifier_builder = VERIFIERS.fetch(verifier) { raise ArgumentError, "#{verifier} not a valid claim verifier" }
|
57
|
+
verifier_builder.call(options || {}).verify!(context: context)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/jwt/claims.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'claims/audience'
|
4
|
+
require_relative 'claims/expiration'
|
5
|
+
require_relative 'claims/issued_at'
|
6
|
+
require_relative 'claims/issuer'
|
7
|
+
require_relative 'claims/jwt_id'
|
8
|
+
require_relative 'claims/not_before'
|
9
|
+
require_relative 'claims/numeric'
|
10
|
+
require_relative 'claims/required'
|
11
|
+
require_relative 'claims/subject'
|
12
|
+
require_relative 'claims/decode_verifier'
|
13
|
+
require_relative 'claims/verifier'
|
14
|
+
|
15
|
+
module JWT
|
16
|
+
# JWT Claim verifications
|
17
|
+
# https://datatracker.ietf.org/doc/html/rfc7519#section-4
|
18
|
+
#
|
19
|
+
# Verification is supported for the following claims:
|
20
|
+
# exp
|
21
|
+
# nbf
|
22
|
+
# iss
|
23
|
+
# iat
|
24
|
+
# jti
|
25
|
+
# aud
|
26
|
+
# sub
|
27
|
+
# required
|
28
|
+
# numeric
|
29
|
+
#
|
30
|
+
module Claims
|
31
|
+
# Represents a claim verification error
|
32
|
+
Error = Struct.new(:message, keyword_init: true)
|
33
|
+
|
34
|
+
class << self
|
35
|
+
# @deprecated Use {verify_payload!} instead. Will be removed in the next major version of ruby-jwt.
|
36
|
+
def verify!(payload, options)
|
37
|
+
DecodeVerifier.verify!(payload, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Checks if the claims in the JWT payload are valid.
|
41
|
+
# @example
|
42
|
+
#
|
43
|
+
# ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :exp)
|
44
|
+
# ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11})
|
45
|
+
#
|
46
|
+
# @param payload [Hash] the JWT payload.
|
47
|
+
# @param options [Array] the options for verifying the claims.
|
48
|
+
# @return [void]
|
49
|
+
# @raise [JWT::DecodeError] if any claim is invalid.
|
50
|
+
def verify_payload!(payload, *options)
|
51
|
+
verify_token!(VerificationContext.new(payload: payload), *options)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks if the claims in the JWT payload are valid.
|
55
|
+
#
|
56
|
+
# @param payload [Hash] the JWT payload.
|
57
|
+
# @param options [Array] the options for verifying the claims.
|
58
|
+
# @return [Boolean] true if the claims are valid, false otherwise
|
59
|
+
def valid_payload?(payload, *options)
|
60
|
+
payload_errors(payload, *options).empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the errors in the claims of the JWT token.
|
64
|
+
#
|
65
|
+
# @param options [Array] the options for verifying the claims.
|
66
|
+
# @return [Array<JWT::Claims::Error>] the errors in the claims of the JWT
|
67
|
+
def payload_errors(payload, *options)
|
68
|
+
token_errors(VerificationContext.new(payload: payload), *options)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def verify_token!(token, *options)
|
74
|
+
Verifier.verify!(token, *options)
|
75
|
+
end
|
76
|
+
|
77
|
+
def token_errors(token, *options)
|
78
|
+
Verifier.errors(token, *options)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/jwt/claims_validator.rb
CHANGED
@@ -4,34 +4,13 @@ require_relative 'error'
|
|
4
4
|
|
5
5
|
module JWT
|
6
6
|
class ClaimsValidator
|
7
|
-
NUMERIC_CLAIMS = %i[
|
8
|
-
exp
|
9
|
-
iat
|
10
|
-
nbf
|
11
|
-
].freeze
|
12
|
-
|
13
7
|
def initialize(payload)
|
14
|
-
@payload = payload
|
8
|
+
@payload = payload
|
15
9
|
end
|
16
10
|
|
17
11
|
def validate!
|
18
|
-
|
19
|
-
|
12
|
+
Claims.verify_payload!(@payload, :numeric)
|
20
13
|
true
|
21
14
|
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def validate_numeric_claims
|
26
|
-
NUMERIC_CLAIMS.each do |claim|
|
27
|
-
validate_is_numeric(claim) if @payload.key?(claim)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def validate_is_numeric(claim)
|
32
|
-
return if @payload[claim].is_a?(Numeric)
|
33
|
-
|
34
|
-
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
|
35
|
-
end
|
36
15
|
end
|
37
16
|
end
|
data/lib/jwt/decode.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
-
|
5
|
-
require 'jwt/verify'
|
6
4
|
require 'jwt/x5c_key_finder'
|
7
5
|
|
8
6
|
# JWT::Decode module
|
@@ -51,6 +49,7 @@ module JWT
|
|
51
49
|
|
52
50
|
def verify_algo
|
53
51
|
raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
|
52
|
+
raise JWT::DecodeError, 'Token header not a JSON object' unless header.is_a?(Hash)
|
54
53
|
raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
|
55
54
|
raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
|
56
55
|
end
|
@@ -92,7 +91,7 @@ module JWT
|
|
92
91
|
end
|
93
92
|
|
94
93
|
def resolve_allowed_algorithms
|
95
|
-
algs = given_algorithms.map { |alg| JWA.
|
94
|
+
algs = given_algorithms.map { |alg| JWA.resolve(alg) }
|
96
95
|
|
97
96
|
sort_by_alg_header(algs)
|
98
97
|
end
|
@@ -113,8 +112,7 @@ module JWT
|
|
113
112
|
end
|
114
113
|
|
115
114
|
def verify_claims
|
116
|
-
|
117
|
-
Verify.verify_required_claims(payload, @options)
|
115
|
+
Claims::DecodeVerifier.verify!(payload, @options)
|
118
116
|
end
|
119
117
|
|
120
118
|
def validate_segment_count!
|