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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +60 -53
  3. data/CHANGELOG.md +194 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +360 -106
  7. data/lib/jwt/base64.rb +19 -2
  8. data/lib/jwt/claims/audience.rb +30 -0
  9. data/lib/jwt/claims/crit.rb +35 -0
  10. data/lib/jwt/claims/decode_verifier.rb +40 -0
  11. data/lib/jwt/claims/expiration.rb +32 -0
  12. data/lib/jwt/claims/issued_at.rb +22 -0
  13. data/lib/jwt/claims/issuer.rb +34 -0
  14. data/lib/jwt/claims/jwt_id.rb +35 -0
  15. data/lib/jwt/claims/not_before.rb +32 -0
  16. data/lib/jwt/claims/numeric.rb +77 -0
  17. data/lib/jwt/claims/required.rb +33 -0
  18. data/lib/jwt/claims/subject.rb +30 -0
  19. data/lib/jwt/claims/verification_methods.rb +20 -0
  20. data/lib/jwt/claims/verifier.rb +61 -0
  21. data/lib/jwt/claims.rb +74 -0
  22. data/lib/jwt/claims_validator.rb +7 -24
  23. data/lib/jwt/configuration/container.rb +52 -0
  24. data/lib/jwt/configuration/decode_configuration.rb +70 -0
  25. data/lib/jwt/configuration/jwk_configuration.rb +28 -0
  26. data/lib/jwt/configuration.rb +23 -0
  27. data/lib/jwt/decode.rb +70 -61
  28. data/lib/jwt/deprecations.rb +49 -0
  29. data/lib/jwt/encode.rb +18 -57
  30. data/lib/jwt/encoded_token.rb +139 -0
  31. data/lib/jwt/error.rb +36 -0
  32. data/lib/jwt/json.rb +1 -1
  33. data/lib/jwt/jwa/compat.rb +32 -0
  34. data/lib/jwt/jwa/ecdsa.rb +90 -0
  35. data/lib/jwt/jwa/eddsa.rb +35 -0
  36. data/lib/jwt/jwa/hmac.rb +82 -0
  37. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  38. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
  39. data/lib/jwt/jwa/none.rb +24 -0
  40. data/lib/jwt/jwa/ps.rb +35 -0
  41. data/lib/jwt/jwa/rsa.rb +35 -0
  42. data/lib/jwt/jwa/signing_algorithm.rb +63 -0
  43. data/lib/jwt/jwa/unsupported.rb +20 -0
  44. data/lib/jwt/jwa/wrapper.rb +44 -0
  45. data/lib/jwt/jwa.rb +58 -0
  46. data/lib/jwt/jwk/ec.rb +163 -63
  47. data/lib/jwt/jwk/hmac.rb +68 -24
  48. data/lib/jwt/jwk/key_base.rb +46 -6
  49. data/lib/jwt/jwk/key_finder.rb +20 -35
  50. data/lib/jwt/jwk/kid_as_key_digest.rb +16 -0
  51. data/lib/jwt/jwk/okp_rbnacl.rb +109 -0
  52. data/lib/jwt/jwk/rsa.rb +141 -54
  53. data/lib/jwt/jwk/set.rb +82 -0
  54. data/lib/jwt/jwk/thumbprint.rb +26 -0
  55. data/lib/jwt/jwk.rb +16 -11
  56. data/lib/jwt/token.rb +112 -0
  57. data/lib/jwt/verify.rb +16 -81
  58. data/lib/jwt/version.rb +53 -11
  59. data/lib/jwt/x5c_key_finder.rb +52 -0
  60. data/lib/jwt.rb +28 -4
  61. data/ruby-jwt.gemspec +15 -5
  62. metadata +75 -28
  63. data/.github/workflows/test.yml +0 -74
  64. data/.gitignore +0 -11
  65. data/.rspec +0 -2
  66. data/.rubocop.yml +0 -97
  67. data/.rubocop_todo.yml +0 -185
  68. data/.sourcelevel.yml +0 -18
  69. data/Appraisals +0 -10
  70. data/Gemfile +0 -5
  71. data/Rakefile +0 -14
  72. data/lib/jwt/algos/ecdsa.rb +0 -35
  73. data/lib/jwt/algos/eddsa.rb +0 -30
  74. data/lib/jwt/algos/hmac.rb +0 -34
  75. data/lib/jwt/algos/none.rb +0 -15
  76. data/lib/jwt/algos/ps.rb +0 -43
  77. data/lib/jwt/algos/rsa.rb +0 -19
  78. data/lib/jwt/algos/unsupported.rb +0 -17
  79. data/lib/jwt/algos.rb +0 -44
  80. data/lib/jwt/default_options.rb +0 -16
  81. data/lib/jwt/security_utils.rb +0 -57
  82. data/lib/jwt/signature.rb +0 -39
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'decode_configuration'
4
+ require_relative 'jwk_configuration'
5
+
6
+ module JWT
7
+ module Configuration
8
+ # The Container class holds the configuration settings for JWT.
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.
16
+ attr_accessor :decode, :jwk, :strict_base64_decoding
17
+
18
+ # @!attribute [r] deprecation_warnings
19
+ # @return [Symbol] the deprecation warnings setting.
20
+ attr_reader :deprecation_warnings
21
+
22
+ # Initializes a new Container instance and resets the configuration.
23
+ def initialize
24
+ reset!
25
+ end
26
+
27
+ # Resets the configuration to default values.
28
+ #
29
+ # @return [void]
30
+ def reset!
31
+ @decode = DecodeConfiguration.new
32
+ @jwk = JwkConfiguration.new
33
+ @strict_base64_decoding = false
34
+
35
+ self.deprecation_warnings = :once
36
+ end
37
+
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]
45
+ def deprecation_warnings=(value)
46
+ raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value)
47
+
48
+ @deprecation_warnings = value
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Configuration
5
+ # The DecodeConfiguration class holds the configuration settings for decoding JWT tokens.
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
+
28
+ attr_accessor :verify_expiration,
29
+ :verify_not_before,
30
+ :verify_iss,
31
+ :verify_iat,
32
+ :verify_jti,
33
+ :verify_aud,
34
+ :verify_sub,
35
+ :leeway,
36
+ :algorithms,
37
+ :required_claims
38
+
39
+ # Initializes a new DecodeConfiguration instance with default settings.
40
+ def initialize
41
+ @verify_expiration = true
42
+ @verify_not_before = true
43
+ @verify_iss = false
44
+ @verify_iat = false
45
+ @verify_jti = false
46
+ @verify_aud = false
47
+ @verify_sub = false
48
+ @leeway = 0
49
+ @algorithms = ['HS256']
50
+ @required_claims = []
51
+ end
52
+
53
+ # @api private
54
+ def to_h
55
+ {
56
+ verify_expiration: verify_expiration,
57
+ verify_not_before: verify_not_before,
58
+ verify_iss: verify_iss,
59
+ verify_iat: verify_iat,
60
+ verify_jti: verify_jti,
61
+ verify_aud: verify_aud,
62
+ verify_sub: verify_sub,
63
+ leeway: leeway,
64
+ algorithms: algorithms,
65
+ required_claims: required_claims
66
+ }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,28 @@
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
+ # @api private
9
+ class JwkConfiguration
10
+ def initialize
11
+ self.kid_generator_type = :key_digest
12
+ end
13
+
14
+ def kid_generator_type=(value)
15
+ self.kid_generator = case value
16
+ when :key_digest
17
+ JWT::JWK::KidAsKeyDigest
18
+ when :rfc7638_thumbprint
19
+ JWT::JWK::Thumbprint
20
+ else
21
+ raise ArgumentError, "#{value} is not a valid kid generator type."
22
+ end
23
+ end
24
+
25
+ attr_accessor :kid_generator
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration/container'
4
+
5
+ module JWT
6
+ # The Configuration module provides methods to configure JWT settings.
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.
12
+ def configure
13
+ yield(configuration)
14
+ end
15
+
16
+ # Returns the JWT configuration container.
17
+ #
18
+ # @return [JWT::Configuration::Container] the configuration container.
19
+ def configuration
20
+ @configuration ||= ::JWT::Configuration::Container.new
21
+ end
22
+ end
23
+ end
data/lib/jwt/decode.rb CHANGED
@@ -1,111 +1,120 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
+ require 'jwt/x5c_key_finder'
4
5
 
5
- require 'jwt/signature'
6
- require 'jwt/verify'
7
- # JWT::Decode module
8
6
  module JWT
9
- # Decoding logic for JWT
7
+ # The Decode class is responsible for decoding and verifying JWT tokens.
10
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.
11
17
  def initialize(jwt, key, verify, options, &keyfinder)
12
- raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
13
- @jwt = jwt
18
+ raise JWT::DecodeError, 'Nil JSON web token' unless jwt
19
+
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_crypto
33
+ verify_algo
34
+ set_key
26
35
  verify_signature
27
- verify_claims
36
+ Claims::DecodeVerifier.verify!(token.payload, @options)
28
37
  end
29
- raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
30
- [payload, header]
38
+
39
+ [token.payload, token.header]
31
40
  end
32
41
 
33
42
  private
34
43
 
44
+ attr_reader :token
45
+
35
46
  def verify_signature
36
- raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
37
- raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless header['alg']
38
- raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
47
+ return if none_algorithm?
39
48
 
40
- @key = find_key(&@keyfinder) if @keyfinder
41
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
49
+ raise JWT::DecodeError, 'No verification key available' unless @key
42
50
 
43
- Signature.verify(header['alg'], @key, signing_input, @signature)
51
+ token.verify_signature!(algorithm: allowed_and_valid_algorithms, key: @key)
44
52
  end
45
53
 
46
- def options_includes_algo_in_header?
47
- allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
54
+ def verify_algo
55
+ raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
56
+ raise JWT::DecodeError, 'Token header not a JSON object' unless token.header.is_a?(Hash)
57
+ raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
58
+ raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
48
59
  end
49
60
 
50
- def allowed_algorithms
51
- # Order is very important - first check for string keys, next for symbols
52
- algos = if @options.key?('algorithm')
53
- @options['algorithm']
54
- elsif @options.key?(:algorithm)
55
- @options[:algorithm]
56
- elsif @options.key?('algorithms')
57
- @options['algorithms']
58
- elsif @options.key?(:algorithms)
59
- @options[:algorithms]
60
- else
61
- []
62
- end
63
- Array(algos)
64
- end
61
+ def set_key
62
+ @key = find_key(&@keyfinder) if @keyfinder
63
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
64
+ return unless (x5c_options = @options[:x5c])
65
65
 
66
- def find_key(&keyfinder)
67
- key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
68
- raise JWT::DecodeError, 'No verification key available' unless key
69
- key
66
+ @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
70
67
  end
71
68
 
72
- def verify_claims
73
- Verify.verify_claims(payload, @options)
74
- Verify.verify_required_claims(payload, @options)
69
+ def allowed_and_valid_algorithms
70
+ @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
75
71
  end
76
72
 
77
- def validate_segment_count!
78
- return if segment_length == 3
79
- return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
80
- return if segment_length == 2 && header['alg'] == 'none'
73
+ # Order is very important - first check for string keys, next for symbols
74
+ ALGORITHM_KEYS = ['algorithm',
75
+ :algorithm,
76
+ 'algorithms',
77
+ :algorithms].freeze
81
78
 
82
- raise(JWT::DecodeError, 'Not enough or too many segments')
79
+ def given_algorithms
80
+ ALGORITHM_KEYS.each do |alg_key|
81
+ alg = @options[alg_key]
82
+ return Array(alg) if alg
83
+ end
84
+ []
83
85
  end
84
86
 
85
- def segment_length
86
- @segments.count
87
+ def allowed_algorithms
88
+ @allowed_algorithms ||= resolve_allowed_algorithms
87
89
  end
88
90
 
89
- def decode_crypto
90
- @signature = JWT::Base64.url_decode(@segments[2] || '')
91
+ def resolve_allowed_algorithms
92
+ given_algorithms.map { |alg| JWA.resolve(alg) }
91
93
  end
92
94
 
93
- def header
94
- @header ||= parse_and_decode @segments[0]
95
+ def find_key(&keyfinder)
96
+ key = (keyfinder.arity == 2 ? yield(token.header, token.payload) : yield(token.header))
97
+ # key can be of type [string, nil, OpenSSL::PKey, Array]
98
+ return key if key && !Array(key).empty?
99
+
100
+ raise JWT::DecodeError, 'No verification key available'
95
101
  end
96
102
 
97
- def payload
98
- @payload ||= parse_and_decode @segments[1]
103
+ def validate_segment_count!
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?
108
+
109
+ raise JWT::DecodeError, 'Not enough or too many segments'
99
110
  end
100
111
 
101
- def signing_input
102
- @segments.first(2).join('.')
112
+ def none_algorithm?
113
+ alg_in_header == 'none'
103
114
  end
104
115
 
105
- def parse_and_decode(segment)
106
- JWT::JSON.parse(JWT::Base64.url_decode(segment))
107
- rescue ::JSON::ParserError
108
- raise JWT::DecodeError, 'Invalid segment encoding'
116
+ def alg_in_header
117
+ token.header['alg']
109
118
  end
110
119
  end
111
120
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ # Deprecations module to handle deprecation warnings in the gem
5
+ # @api private
6
+ module Deprecations
7
+ class << self
8
+ def context
9
+ yield.tap { emit_warnings }
10
+ ensure
11
+ Thread.current[:jwt_warning_store] = nil
12
+ end
13
+
14
+ def warning(message, only_if_valid: false)
15
+ method_name = only_if_valid ? :store : :warn
16
+ case JWT.configuration.deprecation_warnings
17
+ when :once
18
+ return if record_warned(message)
19
+ when :warn
20
+ # noop
21
+ else
22
+ return
23
+ end
24
+
25
+ send(method_name, "[DEPRECATION WARNING] #{message}")
26
+ end
27
+
28
+ def store(message)
29
+ (Thread.current[:jwt_warning_store] ||= []) << message
30
+ end
31
+
32
+ def emit_warnings
33
+ return if Thread.current[:jwt_warning_store].nil?
34
+
35
+ Thread.current[:jwt_warning_store].each { |warning| warn(warning) }
36
+ end
37
+
38
+ private
39
+
40
+ def record_warned(message)
41
+ @warned ||= []
42
+ return true if @warned.include?(message)
43
+
44
+ @warned << message
45
+ false
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/jwt/encode.rb CHANGED
@@ -1,69 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './algos'
4
- require_relative './claims_validator'
3
+ require_relative 'jwa'
5
4
 
6
- # JWT::Encode module
7
5
  module JWT
8
- # Encoding logic for JWT
6
+ # The Encode class is responsible for encoding JWT tokens.
9
7
  class Encode
10
- ALG_NONE = 'none'.freeze
11
- ALG_KEY = 'alg'.freeze
12
-
8
+ # Initializes a new Encode instance.
9
+ #
10
+ # @param options [Hash] the options for encoding the JWT token.
11
+ # @option options [Hash] :payload the payload of the JWT token.
12
+ # @option options [Hash] :headers the headers of the JWT token.
13
+ # @option options [String] :key the key used to sign the JWT token.
14
+ # @option options [String] :algorithm the algorithm used to sign the JWT token.
13
15
  def initialize(options)
14
- @payload = options[:payload]
15
- @key = options[:key]
16
- _, @algorithm = Algos.find(options[:algorithm])
17
- @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
16
+ @token = Token.new(payload: options[:payload], header: options[:headers])
17
+ @key = options[:key]
18
+ @algorithm = options[:algorithm]
18
19
  end
19
20
 
21
+ # Encodes the JWT token and returns its segments.
22
+ #
23
+ # @return [String] the encoded JWT token.
20
24
  def segments
21
- @segments ||= combine(encoded_header_and_payload, encoded_signature)
22
- end
23
-
24
- private
25
-
26
- def encoded_header
27
- @encoded_header ||= encode_header
28
- end
29
-
30
- def encoded_payload
31
- @encoded_payload ||= encode_payload
32
- end
33
-
34
- def encoded_signature
35
- @encoded_signature ||= encode_signature
36
- end
37
-
38
- def encoded_header_and_payload
39
- @encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
40
- end
41
-
42
- def encode_header
43
- @headers[ALG_KEY] = @algorithm
44
- encode(@headers)
45
- end
46
-
47
- def encode_payload
48
- if @payload && @payload.is_a?(Hash)
49
- ClaimsValidator.new(@payload).validate!
50
- end
51
-
52
- encode(@payload)
53
- end
54
-
55
- def encode_signature
56
- return '' if @algorithm == ALG_NONE
57
-
58
- JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
59
- end
60
-
61
- def encode(data)
62
- JWT::Base64.url_encode(JWT::JSON.generate(data))
63
- end
64
-
65
- def combine(*parts)
66
- parts.join('.')
25
+ @token.verify_claims!(:numeric)
26
+ @token.sign!(algorithm: @algorithm, key: @key)
27
+ @token.jwt
67
28
  end
68
29
  end
69
30
  end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ # Represents an encoded JWT token
5
+ #
6
+ # Processing an encoded and signed token:
7
+ #
8
+ # token = JWT::Token.new(payload: {pay: 'load'})
9
+ # token.sign!(algorithm: 'HS256', key: 'secret')
10
+ #
11
+ # encoded_token = JWT::EncodedToken.new(token.jwt)
12
+ # encoded_token.verify_signature!(algorithm: 'HS256', key: 'secret')
13
+ # encoded_token.payload # => {'pay' => 'load'}
14
+ class EncodedToken
15
+ include Claims::VerificationMethods
16
+
17
+ # Returns the original token provided to the class.
18
+ # @return [String] The JWT token.
19
+ attr_reader :jwt
20
+
21
+ # Initializes a new EncodedToken instance.
22
+ #
23
+ # @param jwt [String] the encoded JWT token.
24
+ # @raise [ArgumentError] if the provided JWT is not a String.
25
+ def initialize(jwt)
26
+ raise ArgumentError, 'Provided JWT must be a String' unless jwt.is_a?(String)
27
+
28
+ @jwt = jwt
29
+ @encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')
30
+ end
31
+
32
+ # Returns the decoded signature of the JWT token.
33
+ #
34
+ # @return [String] the decoded signature.
35
+ def signature
36
+ @signature ||= ::JWT::Base64.url_decode(encoded_signature || '')
37
+ end
38
+
39
+ # Returns the encoded signature of the JWT token.
40
+ #
41
+ # @return [String] the encoded signature.
42
+ attr_reader :encoded_signature
43
+
44
+ # Returns the decoded header of the JWT token.
45
+ #
46
+ # @return [Hash] the header.
47
+ def header
48
+ @header ||= parse_and_decode(@encoded_header)
49
+ end
50
+
51
+ # Returns the encoded header of the JWT token.
52
+ #
53
+ # @return [String] the encoded header.
54
+ attr_reader :encoded_header
55
+
56
+ # Returns the payload of the JWT token.
57
+ #
58
+ # @return [Hash] the payload.
59
+ def payload
60
+ @payload ||= decode_payload
61
+ end
62
+
63
+ # Sets or returns the encoded payload of the JWT token.
64
+ #
65
+ # @return [String] the encoded payload.
66
+ attr_accessor :encoded_payload
67
+
68
+ # Returns the signing input of the JWT token.
69
+ #
70
+ # @return [String] the signing input.
71
+ def signing_input
72
+ [encoded_header, encoded_payload].join('.')
73
+ end
74
+
75
+ # Verifies the signature of the JWT token.
76
+ #
77
+ # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
78
+ # @param key [String, Array<String>] the key(s) to use for verification.
79
+ # @param key_finder [#call] an object responding to `call` to find the key for verification.
80
+ # @return [nil]
81
+ # @raise [JWT::VerificationError] if the signature verification fails.
82
+ # @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
83
+ def verify_signature!(algorithm:, key: nil, key_finder: nil)
84
+ raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
85
+
86
+ key ||= key_finder.call(self)
87
+
88
+ return if valid_signature?(algorithm: algorithm, key: key)
89
+
90
+ raise JWT::VerificationError, 'Signature verification failed'
91
+ end
92
+
93
+ # Checks if the signature of the JWT token is valid.
94
+ #
95
+ # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
96
+ # @param key [String, Array<String>] the key(s) to use for verification.
97
+ # @return [Boolean] true if the signature is valid, false otherwise.
98
+ def valid_signature?(algorithm:, key:)
99
+ Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo|
100
+ Array(key).any? do |one_key|
101
+ algo.verify(data: signing_input, signature: signature, verification_key: one_key)
102
+ end
103
+ end
104
+ end
105
+
106
+ alias to_s jwt
107
+
108
+ private
109
+
110
+ def decode_payload
111
+ raise JWT::DecodeError, 'Encoded payload is empty' if encoded_payload == ''
112
+
113
+ if unencoded_payload?
114
+ verify_claims!(crit: ['b64'])
115
+ return parse_unencoded(encoded_payload)
116
+ end
117
+
118
+ parse_and_decode(encoded_payload)
119
+ end
120
+
121
+ def unencoded_payload?
122
+ header['b64'] == false
123
+ end
124
+
125
+ def parse_and_decode(segment)
126
+ parse(::JWT::Base64.url_decode(segment || ''))
127
+ end
128
+
129
+ def parse_unencoded(segment)
130
+ parse(segment)
131
+ end
132
+
133
+ def parse(segment)
134
+ JWT::JSON.parse(segment)
135
+ rescue ::JSON::ParserError
136
+ raise JWT::DecodeError, 'Invalid segment encoding'
137
+ end
138
+ end
139
+ end