jwt 2.4.1 → 2.6.0

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -2
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +135 -31
  5. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  6. data/lib/jwt/algos/ecdsa.rb +2 -4
  7. data/lib/jwt/algos/eddsa.rb +4 -4
  8. data/lib/jwt/algos/hmac.rb +54 -17
  9. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  10. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  11. data/lib/jwt/algos/none.rb +3 -1
  12. data/lib/jwt/algos/ps.rb +3 -5
  13. data/lib/jwt/algos/rsa.rb +3 -4
  14. data/lib/jwt/algos.rb +38 -15
  15. data/lib/jwt/base64.rb +19 -0
  16. data/lib/jwt/configuration/container.rb +21 -0
  17. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  18. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  19. data/lib/jwt/configuration.rb +15 -0
  20. data/lib/jwt/decode.rb +48 -27
  21. data/lib/jwt/encode.rb +30 -20
  22. data/lib/jwt/jwk/ec.rb +131 -62
  23. data/lib/jwt/jwk/hmac.rb +59 -24
  24. data/lib/jwt/jwk/key_base.rb +43 -7
  25. data/lib/jwt/jwk/key_finder.rb +14 -34
  26. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  27. data/lib/jwt/jwk/rsa.rb +128 -53
  28. data/lib/jwt/jwk/set.rb +80 -0
  29. data/lib/jwt/jwk/thumbprint.rb +26 -0
  30. data/lib/jwt/jwk.rb +13 -11
  31. data/lib/jwt/security_utils.rb +0 -27
  32. data/lib/jwt/version.rb +23 -2
  33. data/lib/jwt/x5c_key_finder.rb +1 -1
  34. data/lib/jwt.rb +5 -4
  35. data/ruby-jwt.gemspec +8 -4
  36. metadata +15 -30
  37. data/.codeclimate.yml +0 -8
  38. data/.github/workflows/coverage.yml +0 -27
  39. data/.github/workflows/test.yml +0 -66
  40. data/.gitignore +0 -13
  41. data/.reek.yml +0 -22
  42. data/.rspec +0 -2
  43. data/.rubocop.yml +0 -67
  44. data/.sourcelevel.yml +0 -17
  45. data/Appraisals +0 -13
  46. data/Gemfile +0 -7
  47. data/Rakefile +0 -16
  48. data/lib/jwt/default_options.rb +0 -18
  49. data/lib/jwt/signature.rb +0 -35
data/lib/jwt/algos/ps.rb CHANGED
@@ -9,11 +9,9 @@ module JWT
9
9
 
10
10
  SUPPORTED = %w[PS256 PS384 PS512].freeze
11
11
 
12
- def sign(to_sign)
12
+ def sign(algorithm, msg, key)
13
13
  require_openssl!
14
14
 
15
- algorithm, msg, key = to_sign.values
16
-
17
15
  key_class = key.class
18
16
 
19
17
  raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String
@@ -23,10 +21,10 @@ module JWT
23
21
  key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
24
22
  end
25
23
 
26
- def verify(to_verify)
24
+ def verify(algorithm, public_key, signing_input, signature)
27
25
  require_openssl!
28
26
 
29
- SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
27
+ SecurityUtils.verify_ps(algorithm, public_key, signing_input, signature)
30
28
  end
31
29
 
32
30
  def require_openssl!
data/lib/jwt/algos/rsa.rb CHANGED
@@ -7,15 +7,14 @@ module JWT
7
7
 
8
8
  SUPPORTED = %w[RS256 RS384 RS512].freeze
9
9
 
10
- def sign(to_sign)
11
- algorithm, msg, key = to_sign.values
10
+ def sign(algorithm, msg, key)
12
11
  raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.instance_of?(String)
13
12
 
14
13
  key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
15
14
  end
16
15
 
17
- def verify(to_verify)
18
- SecurityUtils.verify_rsa(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
16
+ def verify(algorithm, public_key, signing_input, signature)
17
+ SecurityUtils.verify_rsa(algorithm, public_key, signing_input, signature)
19
18
  end
20
19
  end
21
20
  end
data/lib/jwt/algos.rb CHANGED
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ begin
4
+ require 'rbnacl'
5
+ rescue LoadError
6
+ raise if defined?(RbNaCl)
7
+ end
8
+ require 'openssl'
9
+
10
+ require 'jwt/security_utils'
3
11
  require 'jwt/algos/hmac'
4
12
  require 'jwt/algos/eddsa'
5
13
  require 'jwt/algos/ecdsa'
@@ -7,35 +15,50 @@ require 'jwt/algos/rsa'
7
15
  require 'jwt/algos/ps'
8
16
  require 'jwt/algos/none'
9
17
  require 'jwt/algos/unsupported'
18
+ require 'jwt/algos/algo_wrapper'
10
19
 
11
- # JWT::Signature module
12
20
  module JWT
13
- # Signature logic for JWT
14
21
  module Algos
15
22
  extend self
16
23
 
17
- ALGOS = [
18
- Algos::Hmac,
19
- Algos::Ecdsa,
20
- Algos::Rsa,
21
- Algos::Eddsa,
22
- Algos::Ps,
23
- Algos::None,
24
- Algos::Unsupported
25
- ].freeze
24
+ ALGOS = [Algos::Ecdsa,
25
+ Algos::Rsa,
26
+ Algos::Eddsa,
27
+ Algos::Ps,
28
+ Algos::None,
29
+ Algos::Unsupported].tap do |l|
30
+ if ::JWT.rbnacl_6_or_greater?
31
+ require_relative 'algos/hmac_rbnacl'
32
+ l.unshift(Algos::HmacRbNaCl)
33
+ elsif ::JWT.rbnacl?
34
+ require_relative 'algos/hmac_rbnacl_fixed'
35
+ l.unshift(Algos::HmacRbNaClFixed)
36
+ else
37
+ l.unshift(Algos::Hmac)
38
+ end
39
+ end.freeze
26
40
 
27
41
  def find(algorithm)
28
42
  indexed[algorithm && algorithm.downcase]
29
43
  end
30
44
 
45
+ def create(algorithm)
46
+ Algos::AlgoWrapper.new(*find(algorithm))
47
+ end
48
+
49
+ def implementation?(algorithm)
50
+ (algorithm.respond_to?(:valid_alg?) && algorithm.respond_to?(:verify)) ||
51
+ (algorithm.respond_to?(:alg) && algorithm.respond_to?(:sign))
52
+ end
53
+
31
54
  private
32
55
 
33
56
  def indexed
34
57
  @indexed ||= begin
35
- fallback = [Algos::Unsupported, nil]
36
- ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash|
37
- alg.const_get(:SUPPORTED).each do |code|
38
- hash[code.downcase] = [alg, code]
58
+ fallback = [nil, Algos::Unsupported]
59
+ ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
60
+ cls.const_get(:SUPPORTED).each do |alg|
61
+ hash[alg.downcase] = [alg, cls]
39
62
  end
40
63
  end
41
64
  end
data/lib/jwt/base64.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module JWT
6
+ # Base64 helpers
7
+ class Base64
8
+ class << self
9
+ def url_encode(str)
10
+ ::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
11
+ end
12
+
13
+ def url_decode(str)
14
+ str += '=' * (4 - str.length.modulo(4))
15
+ ::Base64.decode64(str.tr('-_', '+/'))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
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
10
+
11
+ def initialize
12
+ reset!
13
+ end
14
+
15
+ def reset!
16
+ @decode = DecodeConfiguration.new
17
+ @jwk = JwkConfiguration.new
18
+ end
19
+ end
20
+ end
21
+ 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
data/lib/jwt/decode.rb CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- require 'jwt/signature'
6
5
  require 'jwt/verify'
7
6
  require 'jwt/x5c_key_finder'
7
+
8
8
  # JWT::Decode module
9
9
  module JWT
10
10
  # Decoding logic for JWT
@@ -24,7 +24,7 @@ module JWT
24
24
  def decode_segments
25
25
  validate_segment_count!
26
26
  if @verify
27
- decode_crypto
27
+ decode_signature
28
28
  verify_algo
29
29
  set_key
30
30
  verify_signature
@@ -51,8 +51,8 @@ module JWT
51
51
 
52
52
  def verify_algo
53
53
  raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
54
- raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
55
- raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
54
+ raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
55
+ raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless valid_alg_in_header?
56
56
  end
57
57
 
58
58
  def set_key
@@ -64,27 +64,50 @@ module JWT
64
64
  end
65
65
 
66
66
  def verify_signature_for?(key)
67
- Signature.verify(algorithm, key, signing_input, @signature)
67
+ allowed_algorithms.any? do |alg|
68
+ alg.verify(data: signing_input, signature: @signature, verification_key: key)
69
+ end
70
+ end
71
+
72
+ def valid_alg_in_header?
73
+ allowed_algorithms.any? { |alg| alg.valid_alg?(alg_in_header) }
68
74
  end
69
75
 
70
- def options_includes_algo_in_header?
71
- allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
76
+ # Order is very important - first check for string keys, next for symbols
77
+ ALGORITHM_KEYS = ['algorithm',
78
+ :algorithm,
79
+ 'algorithms',
80
+ :algorithms].freeze
81
+
82
+ def given_algorithms
83
+ ALGORITHM_KEYS.each do |alg_key|
84
+ alg = @options[alg_key]
85
+ return Array(alg) if alg
86
+ end
87
+ []
72
88
  end
73
89
 
74
90
  def allowed_algorithms
75
- # Order is very important - first check for string keys, next for symbols
76
- algos = if @options.key?('algorithm')
77
- @options['algorithm']
78
- elsif @options.key?(:algorithm)
79
- @options[:algorithm]
80
- elsif @options.key?('algorithms')
81
- @options['algorithms']
82
- elsif @options.key?(:algorithms)
83
- @options[:algorithms]
84
- else
85
- []
91
+ @allowed_algorithms ||= resolve_allowed_algorithms
92
+ end
93
+
94
+ def resolve_allowed_algorithms
95
+ algs = given_algorithms.map do |alg|
96
+ if Algos.implementation?(alg)
97
+ alg
98
+ else
99
+ Algos.create(alg)
100
+ end
86
101
  end
87
- Array(algos)
102
+
103
+ sort_by_alg_header(algs)
104
+ end
105
+
106
+ # Move algorithms matching the JWT alg header to the beginning of the list
107
+ def sort_by_alg_header(algs)
108
+ return algs if algs.size <= 1
109
+
110
+ algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
88
111
  end
89
112
 
90
113
  def find_key(&keyfinder)
@@ -113,16 +136,14 @@ module JWT
113
136
  end
114
137
 
115
138
  def none_algorithm?
116
- algorithm.casecmp('none').zero?
139
+ alg_in_header == 'none'
117
140
  end
118
141
 
119
- def decode_crypto
120
- @signature = Base64.urlsafe_decode64(@segments[2] || '')
121
- rescue ArgumentError
122
- raise(JWT::DecodeError, 'Invalid segment encoding')
142
+ def decode_signature
143
+ @signature = ::JWT::Base64.url_decode(@segments[2] || '')
123
144
  end
124
145
 
125
- def algorithm
146
+ def alg_in_header
126
147
  header['alg']
127
148
  end
128
149
 
@@ -139,8 +160,8 @@ module JWT
139
160
  end
140
161
 
141
162
  def parse_and_decode(segment)
142
- JWT::JSON.parse(Base64.urlsafe_decode64(segment))
143
- rescue ::JSON::ParserError, ArgumentError
163
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
164
+ rescue ::JSON::ParserError
144
165
  raise JWT::DecodeError, 'Invalid segment encoding'
145
166
  end
146
167
  end
data/lib/jwt/encode.rb CHANGED
@@ -1,28 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './algos'
4
- require_relative './claims_validator'
3
+ require_relative 'algos'
4
+ require_relative 'claims_validator'
5
5
 
6
6
  # JWT::Encode module
7
7
  module JWT
8
8
  # Encoding logic for JWT
9
9
  class Encode
10
- ALG_NONE = 'none'
11
- ALG_KEY = 'alg'
10
+ ALG_KEY = 'alg'
12
11
 
13
12
  def initialize(options)
14
- @payload = options[:payload]
15
- @key = options[:key]
16
- _, @algorithm = Algos.find(options[:algorithm])
17
- @headers = options[:headers].transform_keys(&:to_s)
13
+ @payload = options[:payload]
14
+ @key = options[:key]
15
+ @algorithm = resolve_algorithm(options[:algorithm])
16
+ @headers = options[:headers].transform_keys(&:to_s)
17
+ @headers[ALG_KEY] = @algorithm.alg
18
18
  end
19
19
 
20
20
  def segments
21
- @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
+ validate_claims!
22
+ combine(encoded_header_and_payload, encoded_signature)
22
23
  end
23
24
 
24
25
  private
25
26
 
27
+ def resolve_algorithm(algorithm)
28
+ return algorithm if Algos.implementation?(algorithm)
29
+
30
+ Algos.create(algorithm)
31
+ end
32
+
26
33
  def encoded_header
27
34
  @encoded_header ||= encode_header
28
35
  end
@@ -40,26 +47,29 @@ module JWT
40
47
  end
41
48
 
42
49
  def encode_header
43
- @headers[ALG_KEY] = @algorithm
44
- encode(@headers)
50
+ encode_data(@headers)
45
51
  end
46
52
 
47
53
  def encode_payload
48
- if @payload.is_a?(Hash)
49
- ClaimsValidator.new(@payload).validate!
50
- end
54
+ encode_data(@payload)
55
+ end
51
56
 
52
- encode(@payload)
57
+ def signature
58
+ @algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
53
59
  end
54
60
 
55
- def encode_signature
56
- return '' if @algorithm == ALG_NONE
61
+ def validate_claims!
62
+ return unless @payload.is_a?(Hash)
63
+
64
+ ClaimsValidator.new(@payload).validate!
65
+ end
57
66
 
58
- Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false)
67
+ def encode_signature
68
+ ::JWT::Base64.url_encode(signature)
59
69
  end
60
70
 
61
- def encode(data)
62
- Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false)
71
+ def encode_data(data)
72
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
63
73
  end
64
74
 
65
75
  def combine(*parts)