jwt 2.3.0 → 2.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AUTHORS +60 -53
- data/CHANGELOG.md +194 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +360 -106
- data/lib/jwt/base64.rb +19 -2
- data/lib/jwt/claims/audience.rb +30 -0
- data/lib/jwt/claims/crit.rb +35 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +32 -0
- data/lib/jwt/claims/issued_at.rb +22 -0
- data/lib/jwt/claims/issuer.rb +34 -0
- data/lib/jwt/claims/jwt_id.rb +35 -0
- data/lib/jwt/claims/not_before.rb +32 -0
- data/lib/jwt/claims/numeric.rb +77 -0
- data/lib/jwt/claims/required.rb +33 -0
- data/lib/jwt/claims/subject.rb +30 -0
- data/lib/jwt/claims/verification_methods.rb +20 -0
- data/lib/jwt/claims/verifier.rb +61 -0
- data/lib/jwt/claims.rb +74 -0
- data/lib/jwt/claims_validator.rb +7 -24
- data/lib/jwt/configuration/container.rb +52 -0
- data/lib/jwt/configuration/decode_configuration.rb +70 -0
- data/lib/jwt/configuration/jwk_configuration.rb +28 -0
- data/lib/jwt/configuration.rb +23 -0
- data/lib/jwt/decode.rb +70 -61
- data/lib/jwt/deprecations.rb +49 -0
- data/lib/jwt/encode.rb +18 -57
- data/lib/jwt/encoded_token.rb +139 -0
- data/lib/jwt/error.rb +36 -0
- data/lib/jwt/json.rb +1 -1
- data/lib/jwt/jwa/compat.rb +32 -0
- data/lib/jwt/jwa/ecdsa.rb +90 -0
- data/lib/jwt/jwa/eddsa.rb +35 -0
- data/lib/jwt/jwa/hmac.rb +82 -0
- data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
- data/lib/jwt/jwa/none.rb +24 -0
- data/lib/jwt/jwa/ps.rb +35 -0
- data/lib/jwt/jwa/rsa.rb +35 -0
- data/lib/jwt/jwa/signing_algorithm.rb +63 -0
- data/lib/jwt/jwa/unsupported.rb +20 -0
- data/lib/jwt/jwa/wrapper.rb +44 -0
- data/lib/jwt/jwa.rb +58 -0
- data/lib/jwt/jwk/ec.rb +163 -63
- data/lib/jwt/jwk/hmac.rb +68 -24
- data/lib/jwt/jwk/key_base.rb +46 -6
- data/lib/jwt/jwk/key_finder.rb +20 -35
- data/lib/jwt/jwk/kid_as_key_digest.rb +16 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +109 -0
- data/lib/jwt/jwk/rsa.rb +141 -54
- data/lib/jwt/jwk/set.rb +82 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +16 -11
- data/lib/jwt/token.rb +112 -0
- data/lib/jwt/verify.rb +16 -81
- data/lib/jwt/version.rb +53 -11
- data/lib/jwt/x5c_key_finder.rb +52 -0
- data/lib/jwt.rb +28 -4
- data/ruby-jwt.gemspec +15 -5
- metadata +75 -28
- data/.github/workflows/test.yml +0 -74
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -97
- data/.rubocop_todo.yml +0 -185
- data/.sourcelevel.yml +0 -18
- data/Appraisals +0 -10
- data/Gemfile +0 -5
- data/Rakefile +0 -14
- data/lib/jwt/algos/ecdsa.rb +0 -35
- data/lib/jwt/algos/eddsa.rb +0 -30
- data/lib/jwt/algos/hmac.rb +0 -34
- data/lib/jwt/algos/none.rb +0 -15
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -19
- data/lib/jwt/algos/unsupported.rb +0 -17
- data/lib/jwt/algos.rb +0 -44
- data/lib/jwt/default_options.rb +0 -16
- data/lib/jwt/security_utils.rb +0 -57
- data/lib/jwt/signature.rb +0 -39
data/lib/jwt/base64.rb
CHANGED
@@ -3,14 +3,31 @@
|
|
3
3
|
require 'base64'
|
4
4
|
|
5
5
|
module JWT
|
6
|
-
# Base64
|
6
|
+
# Base64 encoding and decoding
|
7
|
+
# @api private
|
7
8
|
class Base64
|
8
9
|
class << self
|
10
|
+
# Encode a string with URL-safe Base64 complying with RFC 4648 (not padded).
|
11
|
+
# @api private
|
9
12
|
def url_encode(str)
|
10
|
-
::Base64.
|
13
|
+
::Base64.urlsafe_encode64(str, padding: false)
|
11
14
|
end
|
12
15
|
|
16
|
+
# Decode a string with URL-safe Base64 complying with RFC 4648.
|
17
|
+
# Deprecated support for RFC 2045 remains for now. ("All line breaks or other characters not found in Table 1 must be ignored by decoding software")
|
18
|
+
# @api private
|
13
19
|
def url_decode(str)
|
20
|
+
::Base64.urlsafe_decode64(str)
|
21
|
+
rescue ArgumentError => e
|
22
|
+
raise unless e.message == 'invalid base64'
|
23
|
+
raise Base64DecodeError, 'Invalid base64 encoding' if JWT.configuration.strict_base64_decoding
|
24
|
+
|
25
|
+
loose_urlsafe_decode64(str).tap do
|
26
|
+
Deprecations.warning('Invalid base64 input detected, could be because of invalid padding, trailing whitespaces or newline chars. Graceful handling of invalid input will be dropped in the next major version of ruby-jwt', only_if_valid: true)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def loose_urlsafe_decode64(str)
|
14
31
|
str += '=' * (4 - str.length.modulo(4))
|
15
32
|
::Base64.decode64(str.tr('-_', '+/'))
|
16
33
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Audience class is responsible for validating the audience claim ('aud') in a JWT token.
|
6
|
+
class Audience
|
7
|
+
# Initializes a new Audience instance.
|
8
|
+
#
|
9
|
+
# @param expected_audience [String, Array<String>] the expected audience(s) for the JWT token.
|
10
|
+
def initialize(expected_audience:)
|
11
|
+
@expected_audience = expected_audience
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the audience claim ('aud') in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidAudError] if the audience claim is invalid.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
aud = context.payload['aud']
|
22
|
+
raise JWT::InvalidAudError, "Invalid audience. Expected #{expected_audience}, received #{aud || '<none>'}" if ([*aud] & [*expected_audience]).empty?
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :expected_audience
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# Responsible of validation the crit header
|
6
|
+
class Crit
|
7
|
+
# Initializes a new Crit instance.
|
8
|
+
#
|
9
|
+
# @param expected_crits [String] the expected crit header values for the JWT token.
|
10
|
+
def initialize(expected_crits:)
|
11
|
+
@expected_crits = Array(expected_crits)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the critical claim ('crit') in the JWT token header.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload and header.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidCritError] if the crit claim is invalid.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
raise(JWT::InvalidCritError, 'Crit header missing') unless context.header['crit']
|
22
|
+
raise(JWT::InvalidCritError, 'Crit header should be an array') unless context.header['crit'].is_a?(Array)
|
23
|
+
|
24
|
+
missing = (expected_crits - context.header['crit'])
|
25
|
+
raise(JWT::InvalidCritError, "Crit header missing expected values: #{missing.join(', ')}") if missing.any?
|
26
|
+
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :expected_crits
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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
|
+
# @api private
|
8
|
+
VerificationContext = Struct.new(:payload, keyword_init: true)
|
9
|
+
|
10
|
+
# Verifiers to support the ::JWT.decode method
|
11
|
+
#
|
12
|
+
# @api 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
|
+
# @api 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,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Expiration class is responsible for validating the expiration claim ('exp') in a JWT token.
|
6
|
+
class Expiration
|
7
|
+
# Initializes a new Expiration instance.
|
8
|
+
#
|
9
|
+
# @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the expiration time. Default: 0.
|
10
|
+
def initialize(leeway:)
|
11
|
+
@leeway = leeway || 0
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the expiration claim ('exp') in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::ExpiredSignature] if the token has expired.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
return unless context.payload.is_a?(Hash)
|
22
|
+
return unless context.payload.key?('exp')
|
23
|
+
|
24
|
+
raise JWT::ExpiredSignature, 'Signature has expired' if context.payload['exp'].to_i <= (Time.now.to_i - leeway)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :leeway
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token.
|
6
|
+
class IssuedAt
|
7
|
+
# Verifies the issued at claim ('iat') in the JWT token.
|
8
|
+
#
|
9
|
+
# @param context [Object] the context containing the JWT payload.
|
10
|
+
# @param _args [Hash] additional arguments (not used).
|
11
|
+
# @raise [JWT::InvalidIatError] if the issued at claim is invalid.
|
12
|
+
# @return [nil]
|
13
|
+
def verify!(context:, **_args)
|
14
|
+
return unless context.payload.is_a?(Hash)
|
15
|
+
return unless context.payload.key?('iat')
|
16
|
+
|
17
|
+
iat = context.payload['iat']
|
18
|
+
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(::Numeric) || iat.to_f > Time.now.to_f
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Issuer class is responsible for validating the issuer claim ('iss') in a JWT token.
|
6
|
+
class Issuer
|
7
|
+
# Initializes a new Issuer instance.
|
8
|
+
#
|
9
|
+
# @param issuers [String, Symbol, Array<String, Symbol>] the expected issuer(s) for the JWT token.
|
10
|
+
def initialize(issuers:)
|
11
|
+
@issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the issuer claim ('iss') in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidIssuerError] if the issuer claim is invalid.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
case (iss = context.payload['iss'])
|
22
|
+
when *issuers
|
23
|
+
nil
|
24
|
+
else
|
25
|
+
raise JWT::InvalidIssuerError, "Invalid issuer. Expected #{issuers}, received #{iss || '<none>'}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_reader :issuers
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The JwtId class is responsible for validating the JWT ID claim ('jti') in a JWT token.
|
6
|
+
class JwtId
|
7
|
+
# Initializes a new JwtId instance.
|
8
|
+
#
|
9
|
+
# @param validator [#call] an object responding to `call` to validate the JWT ID.
|
10
|
+
def initialize(validator:)
|
11
|
+
@validator = validator
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the JWT ID claim ('jti') in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidJtiError] if the JWT ID claim is invalid or missing.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
jti = context.payload['jti']
|
22
|
+
if validator.respond_to?(:call)
|
23
|
+
verified = validator.arity == 2 ? validator.call(jti, context.payload) : validator.call(jti)
|
24
|
+
raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
|
25
|
+
elsif jti.to_s.strip.empty?
|
26
|
+
raise(JWT::InvalidJtiError, 'Missing jti')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :validator
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The NotBefore class is responsible for validating the 'nbf' (Not Before) claim in a JWT token.
|
6
|
+
class NotBefore
|
7
|
+
# Initializes a new NotBefore instance.
|
8
|
+
#
|
9
|
+
# @param leeway [Integer] the amount of leeway (in seconds) to allow when validating the 'nbf' claim. Defaults to 0.
|
10
|
+
def initialize(leeway:)
|
11
|
+
@leeway = leeway || 0
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the 'nbf' (Not Before) claim in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::ImmatureSignature] if the 'nbf' claim has not been reached.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
return unless context.payload.is_a?(Hash)
|
22
|
+
return unless context.payload.key?('nbf')
|
23
|
+
|
24
|
+
raise JWT::ImmatureSignature, 'Signature nbf has not been reached' if context.payload['nbf'].to_i > (Time.now.to_i + leeway)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :leeway
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Numeric class is responsible for validating numeric claims in a JWT token.
|
6
|
+
# The numeric claims are: exp, iat and nbf
|
7
|
+
class Numeric
|
8
|
+
# The Compat class provides backward compatibility for numeric claim validation.
|
9
|
+
# @api private
|
10
|
+
class Compat
|
11
|
+
def initialize(payload)
|
12
|
+
@payload = payload
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify!
|
16
|
+
JWT::Claims.verify_payload!(@payload, :numeric)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# List of numeric claims that can be validated.
|
21
|
+
NUMERIC_CLAIMS = %i[
|
22
|
+
exp
|
23
|
+
iat
|
24
|
+
nbf
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
private_constant(:NUMERIC_CLAIMS)
|
28
|
+
|
29
|
+
# @api private
|
30
|
+
def self.new(*args)
|
31
|
+
return super if args.empty?
|
32
|
+
|
33
|
+
Deprecations.warning('Calling ::JWT::Claims::Numeric.new with the payload will be removed in the next major version of ruby-jwt')
|
34
|
+
Compat.new(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Verifies the numeric claims in the JWT context.
|
38
|
+
#
|
39
|
+
# @param context [Object] the context containing the JWT payload.
|
40
|
+
# @raise [JWT::InvalidClaimError] if any numeric claim is invalid.
|
41
|
+
# @return [nil]
|
42
|
+
def verify!(context:)
|
43
|
+
validate_numeric_claims(context.payload)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Verifies the numeric claims in the JWT payload.
|
47
|
+
#
|
48
|
+
# @param payload [Hash] the JWT payload containing the claims.
|
49
|
+
# @param _args [Hash] additional arguments (not used).
|
50
|
+
# @raise [JWT::InvalidClaimError] if any numeric claim is invalid.
|
51
|
+
# @return [nil]
|
52
|
+
# @deprecated The ::JWT::Claims::Numeric.verify! method will be removed in the next major version of ruby-jwt
|
53
|
+
def self.verify!(payload:, **_args)
|
54
|
+
Deprecations.warning('The ::JWT::Claims::Numeric.verify! method will be removed in the next major version of ruby-jwt.')
|
55
|
+
JWT::Claims.verify_payload!(payload, :numeric)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def validate_numeric_claims(payload)
|
61
|
+
NUMERIC_CLAIMS.each do |claim|
|
62
|
+
validate_is_numeric(payload, claim)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def validate_is_numeric(payload, claim)
|
67
|
+
return unless payload.is_a?(Hash)
|
68
|
+
return unless payload.key?(claim) ||
|
69
|
+
payload.key?(claim.to_s)
|
70
|
+
|
71
|
+
return if payload[claim].is_a?(::Numeric) || payload[claim.to_s].is_a?(::Numeric)
|
72
|
+
|
73
|
+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{(payload[claim] || payload[claim.to_s]).class}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Required class is responsible for validating that all required claims are present in a JWT token.
|
6
|
+
class Required
|
7
|
+
# Initializes a new Required instance.
|
8
|
+
#
|
9
|
+
# @param required_claims [Array<String>] the list of required claims.
|
10
|
+
def initialize(required_claims:)
|
11
|
+
@required_claims = required_claims
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies that all required claims are present in the JWT payload.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::MissingRequiredClaim] if any required claim is missing.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
required_claims.each do |required_claim|
|
22
|
+
next if context.payload.is_a?(Hash) && context.payload.key?(required_claim)
|
23
|
+
|
24
|
+
raise JWT::MissingRequiredClaim, "Missing required claim #{required_claim}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :required_claims
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# The Subject class is responsible for validating the subject claim ('sub') in a JWT token.
|
6
|
+
class Subject
|
7
|
+
# Initializes a new Subject instance.
|
8
|
+
#
|
9
|
+
# @param expected_subject [String] the expected subject for the JWT token.
|
10
|
+
def initialize(expected_subject:)
|
11
|
+
@expected_subject = expected_subject.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
# Verifies the subject claim ('sub') in the JWT token.
|
15
|
+
#
|
16
|
+
# @param context [Object] the context containing the JWT payload.
|
17
|
+
# @param _args [Hash] additional arguments (not used).
|
18
|
+
# @raise [JWT::InvalidSubError] if the subject claim is invalid.
|
19
|
+
# @return [nil]
|
20
|
+
def verify!(context:, **_args)
|
21
|
+
sub = context.payload['sub']
|
22
|
+
raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || '<none>'}") unless sub.to_s == expected_subject
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :expected_subject
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# @api private
|
6
|
+
module VerificationMethods
|
7
|
+
def verify_claims!(*options)
|
8
|
+
Verifier.verify!(self, *options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def claim_errors(*options)
|
12
|
+
Verifier.errors(self, *options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def valid_claims?(*options)
|
16
|
+
claim_errors(*options).empty?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Claims
|
5
|
+
# @api 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
|
+
crit: ->(options) { Claims::Crit.new(expected_crits: options[:crit]) },
|
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
|
+
# @api 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
|
+
# @api 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
|
+
|
44
|
+
def iterate_verifiers(*options)
|
45
|
+
options.each do |element|
|
46
|
+
if element.is_a?(Hash)
|
47
|
+
element.each_key { |key| yield(key, element) }
|
48
|
+
else
|
49
|
+
yield(element, {})
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def verify_one!(context, verifier, options)
|
55
|
+
verifier_builder = VERIFIERS.fetch(verifier) { raise ArgumentError, "#{verifier} not a valid claim verifier" }
|
56
|
+
verifier_builder.call(options || {}).verify!(context: context)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/jwt/claims.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'claims/audience'
|
4
|
+
require_relative 'claims/crit'
|
5
|
+
require_relative 'claims/decode_verifier'
|
6
|
+
require_relative 'claims/expiration'
|
7
|
+
require_relative 'claims/issued_at'
|
8
|
+
require_relative 'claims/issuer'
|
9
|
+
require_relative 'claims/jwt_id'
|
10
|
+
require_relative 'claims/not_before'
|
11
|
+
require_relative 'claims/numeric'
|
12
|
+
require_relative 'claims/required'
|
13
|
+
require_relative 'claims/subject'
|
14
|
+
require_relative 'claims/verification_methods'
|
15
|
+
require_relative 'claims/verifier'
|
16
|
+
|
17
|
+
module JWT
|
18
|
+
# JWT Claim verifications
|
19
|
+
# https://datatracker.ietf.org/doc/html/rfc7519#section-4
|
20
|
+
#
|
21
|
+
# Verification is supported for the following claims:
|
22
|
+
# exp
|
23
|
+
# nbf
|
24
|
+
# iss
|
25
|
+
# iat
|
26
|
+
# jti
|
27
|
+
# aud
|
28
|
+
# sub
|
29
|
+
# required
|
30
|
+
# numeric
|
31
|
+
module Claims
|
32
|
+
# Represents a claim verification error
|
33
|
+
Error = Struct.new(:message, keyword_init: true)
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# @deprecated Use {verify_payload!} instead. Will be removed in the next major version of ruby-jwt.
|
37
|
+
def verify!(payload, options)
|
38
|
+
Deprecations.warning('The ::JWT::Claims.verify! method is deprecated will be removed in the next major version of ruby-jwt')
|
39
|
+
DecodeVerifier.verify!(payload, options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Checks if the claims in the JWT payload are valid.
|
43
|
+
# @example
|
44
|
+
#
|
45
|
+
# ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i + 10}, :exp)
|
46
|
+
# ::JWT::Claims.verify_payload!({"exp" => Time.now.to_i - 10}, exp: { leeway: 11})
|
47
|
+
#
|
48
|
+
# @param payload [Hash] the JWT payload.
|
49
|
+
# @param options [Array] the options for verifying the claims.
|
50
|
+
# @return [void]
|
51
|
+
# @raise [JWT::DecodeError] if any claim is invalid.
|
52
|
+
def verify_payload!(payload, *options)
|
53
|
+
Verifier.verify!(VerificationContext.new(payload: payload), *options)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Checks if the claims in the JWT payload are valid.
|
57
|
+
#
|
58
|
+
# @param payload [Hash] the JWT payload.
|
59
|
+
# @param options [Array] the options for verifying the claims.
|
60
|
+
# @return [Boolean] true if the claims are valid, false otherwise
|
61
|
+
def valid_payload?(payload, *options)
|
62
|
+
payload_errors(payload, *options).empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the errors in the claims of the JWT token.
|
66
|
+
#
|
67
|
+
# @param options [Array] the options for verifying the claims.
|
68
|
+
# @return [Array<JWT::Claims::Error>] the errors in the claims of the JWT
|
69
|
+
def payload_errors(payload, *options)
|
70
|
+
Verifier.errors(VerificationContext.new(payload: payload), *options)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/jwt/claims_validator.rb
CHANGED
@@ -1,35 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JWT
|
4
|
+
# @deprecated Use `Claims.verify_payload!` directly instead.
|
4
5
|
class ClaimsValidator
|
5
|
-
|
6
|
-
exp
|
7
|
-
iat
|
8
|
-
nbf
|
9
|
-
].freeze
|
10
|
-
|
6
|
+
# @deprecated Use `Claims.verify_payload!` directly instead.
|
11
7
|
def initialize(payload)
|
12
|
-
|
8
|
+
Deprecations.warning('The ::JWT::ClaimsValidator class is deprecated and will be removed in the next major version of ruby-jwt')
|
9
|
+
@payload = payload
|
13
10
|
end
|
14
11
|
|
12
|
+
# @deprecated Use `Claims.verify_payload!` directly instead.
|
15
13
|
def validate!
|
16
|
-
|
17
|
-
|
14
|
+
Claims.verify_payload!(@payload, :numeric)
|
18
15
|
true
|
19
16
|
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def validate_numeric_claims
|
24
|
-
NUMERIC_CLAIMS.each do |claim|
|
25
|
-
validate_is_numeric(claim) if @payload.key?(claim)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def validate_is_numeric(claim)
|
30
|
-
return if @payload[claim].is_a?(Numeric)
|
31
|
-
|
32
|
-
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
|
33
|
-
end
|
34
17
|
end
|
35
18
|
end
|