jwt 2.9.2 → 2.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -16
  3. data/README.md +153 -83
  4. data/lib/jwt/base64.rb +3 -0
  5. data/lib/jwt/claims/audience.rb +10 -0
  6. data/lib/jwt/claims/crit.rb +35 -0
  7. data/lib/jwt/claims/decode_verifier.rb +3 -3
  8. data/lib/jwt/claims/expiration.rb +10 -0
  9. data/lib/jwt/claims/issued_at.rb +7 -0
  10. data/lib/jwt/claims/issuer.rb +10 -0
  11. data/lib/jwt/claims/jwt_id.rb +10 -0
  12. data/lib/jwt/claims/not_before.rb +10 -0
  13. data/lib/jwt/claims/numeric.rb +22 -0
  14. data/lib/jwt/claims/required.rb +10 -0
  15. data/lib/jwt/claims/subject.rb +10 -0
  16. data/lib/jwt/claims/verification_methods.rb +20 -0
  17. data/lib/jwt/claims/verifier.rb +6 -7
  18. data/lib/jwt/claims.rb +6 -14
  19. data/lib/jwt/claims_validator.rb +5 -2
  20. data/lib/jwt/configuration/container.rb +20 -0
  21. data/lib/jwt/configuration/decode_configuration.rb +24 -0
  22. data/lib/jwt/configuration/jwk_configuration.rb +1 -0
  23. data/lib/jwt/configuration.rb +8 -0
  24. data/lib/jwt/decode.rb +28 -68
  25. data/lib/jwt/deprecations.rb +1 -0
  26. data/lib/jwt/encode.rb +17 -56
  27. data/lib/jwt/encoded_token.rb +139 -0
  28. data/lib/jwt/error.rb +34 -0
  29. data/lib/jwt/json.rb +1 -1
  30. data/lib/jwt/jwa/compat.rb +3 -0
  31. data/lib/jwt/jwa/ecdsa.rb +3 -6
  32. data/lib/jwt/jwa/eddsa.rb +7 -6
  33. data/lib/jwt/jwa/hmac.rb +2 -3
  34. data/lib/jwt/jwa/hmac_rbnacl.rb +1 -0
  35. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +1 -0
  36. data/lib/jwt/jwa/none.rb +1 -0
  37. data/lib/jwt/jwa/ps.rb +2 -3
  38. data/lib/jwt/jwa/rsa.rb +2 -3
  39. data/lib/jwt/jwa/signing_algorithm.rb +3 -0
  40. data/lib/jwt/jwa/unsupported.rb +1 -0
  41. data/lib/jwt/jwa/wrapper.rb +1 -0
  42. data/lib/jwt/jwa.rb +11 -3
  43. data/lib/jwt/jwk/ec.rb +2 -3
  44. data/lib/jwt/jwk/hmac.rb +2 -3
  45. data/lib/jwt/jwk/key_base.rb +1 -0
  46. data/lib/jwt/jwk/key_finder.rb +1 -0
  47. data/lib/jwt/jwk/kid_as_key_digest.rb +1 -0
  48. data/lib/jwt/jwk/okp_rbnacl.rb +3 -4
  49. data/lib/jwt/jwk/rsa.rb +2 -3
  50. data/lib/jwt/jwk/set.rb +2 -0
  51. data/lib/jwt/jwk.rb +1 -0
  52. data/lib/jwt/token.rb +112 -0
  53. data/lib/jwt/verify.rb +7 -0
  54. data/lib/jwt/version.rb +33 -10
  55. data/lib/jwt.rb +16 -0
  56. metadata +8 -4
@@ -4,12 +4,12 @@ module JWT
4
4
  module Claims
5
5
  # Context class to contain the data passed to individual claim validators
6
6
  #
7
- # @private
7
+ # @api private
8
8
  VerificationContext = Struct.new(:payload, keyword_init: true)
9
9
 
10
10
  # Verifiers to support the ::JWT.decode method
11
11
  #
12
- # @private
12
+ # @api private
13
13
  module DecodeVerifier
14
14
  VERIFIERS = {
15
15
  verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) },
@@ -25,7 +25,7 @@ module JWT
25
25
  private_constant(:VERIFIERS)
26
26
 
27
27
  class << self
28
- # @private
28
+ # @api private
29
29
  def verify!(payload, options)
30
30
  VERIFIERS.each do |key, verifier_builder|
31
31
  next unless options[key] || options[key.to_s]
@@ -2,11 +2,21 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The Expiration class is responsible for validating the expiration claim ('exp') in a JWT token.
5
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.
6
10
  def initialize(leeway:)
7
11
  @leeway = leeway || 0
8
12
  end
9
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]
10
20
  def verify!(context:, **_args)
11
21
  return unless context.payload.is_a?(Hash)
12
22
  return unless context.payload.key?('exp')
@@ -2,7 +2,14 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The IssuedAt class is responsible for validating the issued at claim ('iat') in a JWT token.
5
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]
6
13
  def verify!(context:, **_args)
7
14
  return unless context.payload.is_a?(Hash)
8
15
  return unless context.payload.key?('iat')
@@ -2,11 +2,21 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The Issuer class is responsible for validating the issuer claim ('iss') in a JWT token.
5
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.
6
10
  def initialize(issuers:)
7
11
  @issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
8
12
  end
9
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]
10
20
  def verify!(context:, **_args)
11
21
  case (iss = context.payload['iss'])
12
22
  when *issuers
@@ -2,11 +2,21 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The JwtId class is responsible for validating the JWT ID claim ('jti') in a JWT token.
5
6
  class JwtId
7
+ # Initializes a new JwtId instance.
8
+ #
9
+ # @param validator [#call] an object responding to `call` to validate the JWT ID.
6
10
  def initialize(validator:)
7
11
  @validator = validator
8
12
  end
9
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]
10
20
  def verify!(context:, **_args)
11
21
  jti = context.payload['jti']
12
22
  if validator.respond_to?(:call)
@@ -2,11 +2,21 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The NotBefore class is responsible for validating the 'nbf' (Not Before) claim in a JWT token.
5
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.
6
10
  def initialize(leeway:)
7
11
  @leeway = leeway || 0
8
12
  end
9
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]
10
20
  def verify!(context:, **_args)
11
21
  return unless context.payload.is_a?(Hash)
12
22
  return unless context.payload.key?('nbf')
@@ -2,7 +2,11 @@
2
2
 
3
3
  module JWT
4
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
5
7
  class Numeric
8
+ # The Compat class provides backward compatibility for numeric claim validation.
9
+ # @api private
6
10
  class Compat
7
11
  def initialize(payload)
8
12
  @payload = payload
@@ -13,23 +17,41 @@ module JWT
13
17
  end
14
18
  end
15
19
 
20
+ # List of numeric claims that can be validated.
16
21
  NUMERIC_CLAIMS = %i[
17
22
  exp
18
23
  iat
19
24
  nbf
20
25
  ].freeze
21
26
 
27
+ private_constant(:NUMERIC_CLAIMS)
28
+
29
+ # @api private
22
30
  def self.new(*args)
23
31
  return super if args.empty?
24
32
 
33
+ Deprecations.warning('Calling ::JWT::Claims::Numeric.new with the payload will be removed in the next major version of ruby-jwt')
25
34
  Compat.new(*args)
26
35
  end
27
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]
28
42
  def verify!(context:)
29
43
  validate_numeric_claims(context.payload)
30
44
  end
31
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
32
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.')
33
55
  JWT::Claims.verify_payload!(payload, :numeric)
34
56
  end
35
57
 
@@ -2,11 +2,21 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The Required class is responsible for validating that all required claims are present in a JWT token.
5
6
  class Required
7
+ # Initializes a new Required instance.
8
+ #
9
+ # @param required_claims [Array<String>] the list of required claims.
6
10
  def initialize(required_claims:)
7
11
  @required_claims = required_claims
8
12
  end
9
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]
10
20
  def verify!(context:, **_args)
11
21
  required_claims.each do |required_claim|
12
22
  next if context.payload.is_a?(Hash) && context.payload.key?(required_claim)
@@ -2,11 +2,21 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
+ # The Subject class is responsible for validating the subject claim ('sub') in a JWT token.
5
6
  class Subject
7
+ # Initializes a new Subject instance.
8
+ #
9
+ # @param expected_subject [String] the expected subject for the JWT token.
6
10
  def initialize(expected_subject:)
7
11
  @expected_subject = expected_subject.to_s
8
12
  end
9
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]
10
20
  def verify!(context:, **_args)
11
21
  sub = context.payload['sub']
12
22
  raise(JWT::InvalidSubError, "Invalid subject. Expected #{expected_subject}, received #{sub || '<none>'}") unless sub.to_s == expected_subject
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module JWT
4
4
  module Claims
5
- # @private
5
+ # @api private
6
6
  module Verifier
7
7
  VERIFIERS = {
8
8
  exp: ->(options) { Claims::Expiration.new(leeway: options.dig(:exp, :leeway)) },
@@ -12,7 +12,7 @@ module JWT
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]) },
15
-
15
+ crit: ->(options) { Claims::Crit.new(expected_crits: options[:crit]) },
16
16
  required: ->(options) { Claims::Required.new(required_claims: options[:required]) },
17
17
  numeric: ->(*) { Claims::Numeric.new }
18
18
  }.freeze
@@ -20,7 +20,7 @@ module JWT
20
20
  private_constant(:VERIFIERS)
21
21
 
22
22
  class << self
23
- # @private
23
+ # @api private
24
24
  def verify!(context, *options)
25
25
  iterate_verifiers(*options) do |verifier, verifier_options|
26
26
  verify_one!(context, verifier, verifier_options)
@@ -28,7 +28,7 @@ module JWT
28
28
  nil
29
29
  end
30
30
 
31
- # @private
31
+ # @api private
32
32
  def errors(context, *options)
33
33
  errors = []
34
34
  iterate_verifiers(*options) do |verifier, verifier_options|
@@ -39,7 +39,8 @@ module JWT
39
39
  errors
40
40
  end
41
41
 
42
- # @private
42
+ private
43
+
43
44
  def iterate_verifiers(*options)
44
45
  options.each do |element|
45
46
  if element.is_a?(Hash)
@@ -50,8 +51,6 @@ module JWT
50
51
  end
51
52
  end
52
53
 
53
- private
54
-
55
54
  def verify_one!(context, verifier, options)
56
55
  verifier_builder = VERIFIERS.fetch(verifier) { raise ArgumentError, "#{verifier} not a valid claim verifier" }
57
56
  verifier_builder.call(options || {}).verify!(context: context)
data/lib/jwt/claims.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'claims/audience'
4
+ require_relative 'claims/crit'
5
+ require_relative 'claims/decode_verifier'
4
6
  require_relative 'claims/expiration'
5
7
  require_relative 'claims/issued_at'
6
8
  require_relative 'claims/issuer'
@@ -9,7 +11,7 @@ require_relative 'claims/not_before'
9
11
  require_relative 'claims/numeric'
10
12
  require_relative 'claims/required'
11
13
  require_relative 'claims/subject'
12
- require_relative 'claims/decode_verifier'
14
+ require_relative 'claims/verification_methods'
13
15
  require_relative 'claims/verifier'
14
16
 
15
17
  module JWT
@@ -26,7 +28,6 @@ module JWT
26
28
  # sub
27
29
  # required
28
30
  # numeric
29
- #
30
31
  module Claims
31
32
  # Represents a claim verification error
32
33
  Error = Struct.new(:message, keyword_init: true)
@@ -34,6 +35,7 @@ module JWT
34
35
  class << self
35
36
  # @deprecated Use {verify_payload!} instead. Will be removed in the next major version of ruby-jwt.
36
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')
37
39
  DecodeVerifier.verify!(payload, options)
38
40
  end
39
41
 
@@ -48,7 +50,7 @@ module JWT
48
50
  # @return [void]
49
51
  # @raise [JWT::DecodeError] if any claim is invalid.
50
52
  def verify_payload!(payload, *options)
51
- verify_token!(VerificationContext.new(payload: payload), *options)
53
+ Verifier.verify!(VerificationContext.new(payload: payload), *options)
52
54
  end
53
55
 
54
56
  # Checks if the claims in the JWT payload are valid.
@@ -65,17 +67,7 @@ module JWT
65
67
  # @param options [Array] the options for verifying the claims.
66
68
  # @return [Array<JWT::Claims::Error>] the errors in the claims of the JWT
67
69
  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)
70
+ Verifier.errors(VerificationContext.new(payload: payload), *options)
79
71
  end
80
72
  end
81
73
  end
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'error'
4
-
5
3
  module JWT
4
+ # @deprecated Use `Claims.verify_payload!` directly instead.
6
5
  class ClaimsValidator
6
+ # @deprecated Use `Claims.verify_payload!` directly instead.
7
7
  def initialize(payload)
8
+ Deprecations.warning('The ::JWT::ClaimsValidator class is deprecated and will be removed in the next major version of ruby-jwt')
8
9
  @payload = payload
9
10
  end
10
11
 
12
+ # @deprecated Use `Claims.verify_payload!` directly instead.
11
13
  def validate!
12
14
  Claims.verify_payload!(@payload, :numeric)
15
+ true
13
16
  end
14
17
  end
15
18
  end
@@ -5,14 +5,28 @@ require_relative 'jwk_configuration'
5
5
 
6
6
  module JWT
7
7
  module Configuration
8
+ # The Container class holds the configuration settings for JWT.
8
9
  class Container
10
+ # @!attribute [rw] decode
11
+ # @return [DecodeConfiguration] the decode configuration.
12
+ # @!attribute [rw] jwk
13
+ # @return [JwkConfiguration] the JWK configuration.
14
+ # @!attribute [rw] strict_base64_decoding
15
+ # @return [Boolean] whether strict Base64 decoding is enabled.
9
16
  attr_accessor :decode, :jwk, :strict_base64_decoding
17
+
18
+ # @!attribute [r] deprecation_warnings
19
+ # @return [Symbol] the deprecation warnings setting.
10
20
  attr_reader :deprecation_warnings
11
21
 
22
+ # Initializes a new Container instance and resets the configuration.
12
23
  def initialize
13
24
  reset!
14
25
  end
15
26
 
27
+ # Resets the configuration to default values.
28
+ #
29
+ # @return [void]
16
30
  def reset!
17
31
  @decode = DecodeConfiguration.new
18
32
  @jwk = JwkConfiguration.new
@@ -22,6 +36,12 @@ module JWT
22
36
  end
23
37
 
24
38
  DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze
39
+ private_constant(:DEPRECATION_WARNINGS_VALUES)
40
+ # Sets the deprecation warnings setting.
41
+ #
42
+ # @param value [Symbol] the deprecation warnings setting. Must be one of `:once`, `:warn`, or `:silent`.
43
+ # @raise [ArgumentError] if the value is not one of the supported values.
44
+ # @return [void]
25
45
  def deprecation_warnings=(value)
26
46
  raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value)
27
47
 
@@ -2,7 +2,29 @@
2
2
 
3
3
  module JWT
4
4
  module Configuration
5
+ # The DecodeConfiguration class holds the configuration settings for decoding JWT tokens.
5
6
  class DecodeConfiguration
7
+ # @!attribute [rw] verify_expiration
8
+ # @return [Boolean] whether to verify the expiration claim.
9
+ # @!attribute [rw] verify_not_before
10
+ # @return [Boolean] whether to verify the not before claim.
11
+ # @!attribute [rw] verify_iss
12
+ # @return [Boolean] whether to verify the issuer claim.
13
+ # @!attribute [rw] verify_iat
14
+ # @return [Boolean] whether to verify the issued at claim.
15
+ # @!attribute [rw] verify_jti
16
+ # @return [Boolean] whether to verify the JWT ID claim.
17
+ # @!attribute [rw] verify_aud
18
+ # @return [Boolean] whether to verify the audience claim.
19
+ # @!attribute [rw] verify_sub
20
+ # @return [Boolean] whether to verify the subject claim.
21
+ # @!attribute [rw] leeway
22
+ # @return [Integer] the leeway in seconds for time-based claims.
23
+ # @!attribute [rw] algorithms
24
+ # @return [Array<String>] the list of acceptable algorithms.
25
+ # @!attribute [rw] required_claims
26
+ # @return [Array<String>] the list of required claims.
27
+
6
28
  attr_accessor :verify_expiration,
7
29
  :verify_not_before,
8
30
  :verify_iss,
@@ -14,6 +36,7 @@ module JWT
14
36
  :algorithms,
15
37
  :required_claims
16
38
 
39
+ # Initializes a new DecodeConfiguration instance with default settings.
17
40
  def initialize
18
41
  @verify_expiration = true
19
42
  @verify_not_before = true
@@ -27,6 +50,7 @@ module JWT
27
50
  @required_claims = []
28
51
  end
29
52
 
53
+ # @api private
30
54
  def to_h
31
55
  {
32
56
  verify_expiration: verify_expiration,
@@ -5,6 +5,7 @@ require_relative '../jwk/thumbprint'
5
5
 
6
6
  module JWT
7
7
  module Configuration
8
+ # @api private
8
9
  class JwkConfiguration
9
10
  def initialize
10
11
  self.kid_generator_type = :key_digest
@@ -3,11 +3,19 @@
3
3
  require_relative 'configuration/container'
4
4
 
5
5
  module JWT
6
+ # The Configuration module provides methods to configure JWT settings.
6
7
  module Configuration
8
+ # Configures the JWT settings.
9
+ #
10
+ # @yield [config] Gives the current configuration to the block.
11
+ # @yieldparam config [JWT::Configuration::Container] the configuration container.
7
12
  def configure
8
13
  yield(configuration)
9
14
  end
10
15
 
16
+ # Returns the JWT configuration container.
17
+ #
18
+ # @return [JWT::Configuration::Container] the configuration container.
11
19
  def configuration
12
20
  @configuration ||= ::JWT::Configuration::Container.new
13
21
  end
data/lib/jwt/decode.rb CHANGED
@@ -3,69 +3,67 @@
3
3
  require 'json'
4
4
  require 'jwt/x5c_key_finder'
5
5
 
6
- # JWT::Decode module
7
6
  module JWT
8
- # Decoding logic for JWT
7
+ # The Decode class is responsible for decoding and verifying JWT tokens.
9
8
  class Decode
9
+ # Initializes a new Decode instance.
10
+ #
11
+ # @param jwt [String] the JWT to decode.
12
+ # @param key [String, Array<String>] the key(s) to use for verification.
13
+ # @param verify [Boolean] whether to verify the token's signature.
14
+ # @param options [Hash] additional options for decoding and verification.
15
+ # @param keyfinder [Proc] an optional key finder block to dynamically find the key for verification.
16
+ # @raise [JWT::DecodeError] if decoding or verification fails.
10
17
  def initialize(jwt, key, verify, options, &keyfinder)
11
18
  raise JWT::DecodeError, 'Nil JSON web token' unless jwt
12
19
 
13
- @jwt = jwt
20
+ @token = EncodedToken.new(jwt)
14
21
  @key = key
15
22
  @options = options
16
- @segments = jwt.split('.')
17
23
  @verify = verify
18
- @signature = ''
19
24
  @keyfinder = keyfinder
20
25
  end
21
26
 
27
+ # Decodes the JWT token and verifies its segments if verification is enabled.
28
+ #
29
+ # @return [Array<Hash>] an array containing the decoded payload and header.
22
30
  def decode_segments
23
31
  validate_segment_count!
24
32
  if @verify
25
- decode_signature
26
33
  verify_algo
27
34
  set_key
28
35
  verify_signature
29
- verify_claims
36
+ Claims::DecodeVerifier.verify!(token.payload, @options)
30
37
  end
31
- raise JWT::DecodeError, 'Not enough or too many segments' unless header && payload
32
38
 
33
- [payload, header]
39
+ [token.payload, token.header]
34
40
  end
35
41
 
36
42
  private
37
43
 
38
- def verify_signature
39
- return unless @key || @verify
44
+ attr_reader :token
40
45
 
46
+ def verify_signature
41
47
  return if none_algorithm?
42
48
 
43
49
  raise JWT::DecodeError, 'No verification key available' unless @key
44
50
 
45
- return if Array(@key).any? { |key| verify_signature_for?(key) }
46
-
47
- raise JWT::VerificationError, 'Signature verification failed'
51
+ token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key)
48
52
  end
49
53
 
50
54
  def verify_algo
51
55
  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)
56
+ raise JWT::DecodeError, 'Token header not a JSON object' unless token.header.is_a?(Hash)
53
57
  raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
54
58
  raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
55
59
  end
56
60
 
57
61
  def set_key
58
62
  @key = find_key(&@keyfinder) if @keyfinder
59
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
63
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
60
64
  return unless (x5c_options = @options[:x5c])
61
65
 
62
- @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
63
- end
64
-
65
- def verify_signature_for?(key)
66
- allowed_and_valid_algorithms.any? do |alg|
67
- alg.verify(data: signing_input, signature: @signature, verification_key: key)
68
- end
66
+ @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
69
67
  end
70
68
 
71
69
  def allowed_and_valid_algorithms
@@ -91,70 +89,32 @@ module JWT
91
89
  end
92
90
 
93
91
  def resolve_allowed_algorithms
94
- algs = given_algorithms.map { |alg| JWA.resolve(alg) }
95
-
96
- sort_by_alg_header(algs)
97
- end
98
-
99
- # Move algorithms matching the JWT alg header to the beginning of the list
100
- def sort_by_alg_header(algs)
101
- return algs if algs.size <= 1
102
-
103
- algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
92
+ given_algorithms.map { |alg| JWA.resolve(alg) }
104
93
  end
105
94
 
106
95
  def find_key(&keyfinder)
107
- key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
96
+ key = (keyfinder.arity == 2 ? yield(token.header, token.payload) : yield(token.header))
108
97
  # key can be of type [string, nil, OpenSSL::PKey, Array]
109
98
  return key if key && !Array(key).empty?
110
99
 
111
100
  raise JWT::DecodeError, 'No verification key available'
112
101
  end
113
102
 
114
- def verify_claims
115
- Claims::DecodeVerifier.verify!(payload, @options)
116
- end
117
-
118
103
  def validate_segment_count!
119
- return if segment_length == 3
120
- return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
121
- return if segment_length == 2 && none_algorithm?
104
+ segment_count = token.jwt.count('.') + 1
105
+ return if segment_count == 3
106
+ return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed
107
+ return if segment_count == 2 && none_algorithm?
122
108
 
123
109
  raise JWT::DecodeError, 'Not enough or too many segments'
124
110
  end
125
111
 
126
- def segment_length
127
- @segments.count
128
- end
129
-
130
112
  def none_algorithm?
131
113
  alg_in_header == 'none'
132
114
  end
133
115
 
134
- def decode_signature
135
- @signature = ::JWT::Base64.url_decode(@segments[2] || '')
136
- end
137
-
138
116
  def alg_in_header
139
- header['alg']
140
- end
141
-
142
- def header
143
- @header ||= parse_and_decode @segments[0]
144
- end
145
-
146
- def payload
147
- @payload ||= parse_and_decode @segments[1]
148
- end
149
-
150
- def signing_input
151
- @segments.first(2).join('.')
152
- end
153
-
154
- def parse_and_decode(segment)
155
- JWT::JSON.parse(::JWT::Base64.url_decode(segment))
156
- rescue ::JSON::ParserError
157
- raise JWT::DecodeError, 'Invalid segment encoding'
117
+ token.header['alg']
158
118
  end
159
119
  end
160
120
  end