jwt 2.4.1 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
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)