jwt 2.4.1 → 2.9.3

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +177 -14
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +180 -37
  5. data/lib/jwt/base64.rb +33 -0
  6. data/lib/jwt/claims/audience.rb +20 -0
  7. data/lib/jwt/claims/decode_verifier.rb +40 -0
  8. data/lib/jwt/claims/expiration.rb +22 -0
  9. data/lib/jwt/claims/issued_at.rb +15 -0
  10. data/lib/jwt/claims/issuer.rb +24 -0
  11. data/lib/jwt/claims/jwt_id.rb +25 -0
  12. data/lib/jwt/claims/not_before.rb +22 -0
  13. data/lib/jwt/claims/numeric.rb +55 -0
  14. data/lib/jwt/claims/required.rb +23 -0
  15. data/lib/jwt/claims/subject.rb +20 -0
  16. data/lib/jwt/claims/verifier.rb +62 -0
  17. data/lib/jwt/claims.rb +82 -0
  18. data/lib/jwt/claims_validator.rb +3 -24
  19. data/lib/jwt/configuration/container.rb +32 -0
  20. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  21. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  22. data/lib/jwt/configuration.rb +15 -0
  23. data/lib/jwt/decode.rb +54 -41
  24. data/lib/jwt/deprecations.rb +48 -0
  25. data/lib/jwt/encode.rb +21 -21
  26. data/lib/jwt/error.rb +1 -0
  27. data/lib/jwt/jwa/compat.rb +29 -0
  28. data/lib/jwt/jwa/ecdsa.rb +93 -0
  29. data/lib/jwt/jwa/eddsa.rb +34 -0
  30. data/lib/jwt/jwa/hmac.rb +83 -0
  31. data/lib/jwt/jwa/hmac_rbnacl.rb +49 -0
  32. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  33. data/lib/jwt/jwa/none.rb +23 -0
  34. data/lib/jwt/jwa/ps.rb +36 -0
  35. data/lib/jwt/jwa/rsa.rb +36 -0
  36. data/lib/jwt/jwa/signing_algorithm.rb +60 -0
  37. data/lib/jwt/jwa/unsupported.rb +19 -0
  38. data/lib/jwt/jwa/wrapper.rb +43 -0
  39. data/lib/jwt/jwa.rb +50 -0
  40. data/lib/jwt/jwk/ec.rb +162 -65
  41. data/lib/jwt/jwk/hmac.rb +69 -24
  42. data/lib/jwt/jwk/key_base.rb +45 -7
  43. data/lib/jwt/jwk/key_finder.rb +19 -35
  44. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  45. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  46. data/lib/jwt/jwk/rsa.rb +141 -54
  47. data/lib/jwt/jwk/set.rb +80 -0
  48. data/lib/jwt/jwk/thumbprint.rb +26 -0
  49. data/lib/jwt/jwk.rb +14 -11
  50. data/lib/jwt/verify.rb +10 -89
  51. data/lib/jwt/version.rb +24 -2
  52. data/lib/jwt/x5c_key_finder.rb +3 -6
  53. data/lib/jwt.rb +12 -4
  54. data/ruby-jwt.gemspec +11 -4
  55. metadata +59 -31
  56. data/.codeclimate.yml +0 -8
  57. data/.github/workflows/coverage.yml +0 -27
  58. data/.github/workflows/test.yml +0 -66
  59. data/.gitignore +0 -13
  60. data/.reek.yml +0 -22
  61. data/.rspec +0 -2
  62. data/.rubocop.yml +0 -67
  63. data/.sourcelevel.yml +0 -17
  64. data/Appraisals +0 -13
  65. data/Gemfile +0 -7
  66. data/Rakefile +0 -16
  67. data/lib/jwt/algos/ecdsa.rb +0 -64
  68. data/lib/jwt/algos/eddsa.rb +0 -33
  69. data/lib/jwt/algos/hmac.rb +0 -36
  70. data/lib/jwt/algos/none.rb +0 -17
  71. data/lib/jwt/algos/ps.rb +0 -43
  72. data/lib/jwt/algos/rsa.rb +0 -22
  73. data/lib/jwt/algos/unsupported.rb +0 -19
  74. data/lib/jwt/algos.rb +0 -44
  75. data/lib/jwt/default_options.rb +0 -18
  76. data/lib/jwt/security_utils.rb +0 -59
  77. data/lib/jwt/signature.rb +0 -35
@@ -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
@@ -1,37 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './error'
3
+ 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.transform_keys(&:to_sym)
8
+ @payload = payload
15
9
  end
16
10
 
17
11
  def validate!
18
- validate_numeric_claims
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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decode_configuration'
4
+ require_relative 'jwk_configuration'
5
+
6
+ module JWT
7
+ module Configuration
8
+ class Container
9
+ attr_accessor :decode, :jwk, :strict_base64_decoding
10
+ attr_reader :deprecation_warnings
11
+
12
+ def initialize
13
+ reset!
14
+ end
15
+
16
+ def reset!
17
+ @decode = DecodeConfiguration.new
18
+ @jwk = JwkConfiguration.new
19
+ @strict_base64_decoding = false
20
+
21
+ self.deprecation_warnings = :once
22
+ end
23
+
24
+ DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze
25
+ def deprecation_warnings=(value)
26
+ raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value)
27
+
28
+ @deprecation_warnings = value
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Configuration
5
+ class DecodeConfiguration
6
+ attr_accessor :verify_expiration,
7
+ :verify_not_before,
8
+ :verify_iss,
9
+ :verify_iat,
10
+ :verify_jti,
11
+ :verify_aud,
12
+ :verify_sub,
13
+ :leeway,
14
+ :algorithms,
15
+ :required_claims
16
+
17
+ def initialize
18
+ @verify_expiration = true
19
+ @verify_not_before = true
20
+ @verify_iss = false
21
+ @verify_iat = false
22
+ @verify_jti = false
23
+ @verify_aud = false
24
+ @verify_sub = false
25
+ @leeway = 0
26
+ @algorithms = ['HS256']
27
+ @required_claims = []
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ verify_expiration: verify_expiration,
33
+ verify_not_before: verify_not_before,
34
+ verify_iss: verify_iss,
35
+ verify_iat: verify_iat,
36
+ verify_jti: verify_jti,
37
+ verify_aud: verify_aud,
38
+ verify_sub: verify_sub,
39
+ leeway: leeway,
40
+ algorithms: algorithms,
41
+ required_claims: required_claims
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../jwk/kid_as_key_digest'
4
+ require_relative '../jwk/thumbprint'
5
+
6
+ module JWT
7
+ module Configuration
8
+ class JwkConfiguration
9
+ def initialize
10
+ self.kid_generator_type = :key_digest
11
+ end
12
+
13
+ def kid_generator_type=(value)
14
+ self.kid_generator = case value
15
+ when :key_digest
16
+ JWT::JWK::KidAsKeyDigest
17
+ when :rfc7638_thumbprint
18
+ JWT::JWK::Thumbprint
19
+ else
20
+ raise ArgumentError, "#{value} is not a valid kid generator type."
21
+ end
22
+ end
23
+
24
+ attr_accessor :kid_generator
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration/container'
4
+
5
+ module JWT
6
+ module Configuration
7
+ def configure
8
+ yield(configuration)
9
+ end
10
+
11
+ def configuration
12
+ @configuration ||= ::JWT::Configuration::Container.new
13
+ end
14
+ end
15
+ end