jwt 1.5.6 → 2.2.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 (68) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +74 -0
  3. data/.gitignore +1 -1
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +95 -0
  6. data/.rubocop_todo.yml +191 -0
  7. data/.sourcelevel.yml +18 -0
  8. data/AUTHORS +101 -0
  9. data/Appraisals +10 -0
  10. data/CHANGELOG.md +349 -8
  11. data/Gemfile +2 -1
  12. data/README.md +225 -68
  13. data/Rakefile +4 -1
  14. data/lib/jwt.rb +14 -176
  15. data/lib/jwt/algos.rb +44 -0
  16. data/lib/jwt/algos/ecdsa.rb +35 -0
  17. data/lib/jwt/algos/eddsa.rb +23 -0
  18. data/lib/jwt/algos/hmac.rb +34 -0
  19. data/lib/jwt/algos/none.rb +15 -0
  20. data/lib/jwt/algos/ps.rb +43 -0
  21. data/lib/jwt/algos/rsa.rb +19 -0
  22. data/lib/jwt/algos/unsupported.rb +17 -0
  23. data/lib/jwt/base64.rb +19 -0
  24. data/lib/jwt/claims_validator.rb +35 -0
  25. data/lib/jwt/decode.rb +83 -31
  26. data/lib/jwt/default_options.rb +15 -0
  27. data/lib/jwt/encode.rb +69 -0
  28. data/lib/jwt/error.rb +6 -0
  29. data/lib/jwt/json.rb +10 -9
  30. data/lib/jwt/jwk.rb +51 -0
  31. data/lib/jwt/jwk/ec.rb +150 -0
  32. data/lib/jwt/jwk/hmac.rb +58 -0
  33. data/lib/jwt/jwk/key_base.rb +18 -0
  34. data/lib/jwt/jwk/key_finder.rb +62 -0
  35. data/lib/jwt/jwk/rsa.rb +115 -0
  36. data/lib/jwt/security_utils.rb +57 -0
  37. data/lib/jwt/signature.rb +39 -0
  38. data/lib/jwt/verify.rb +45 -53
  39. data/lib/jwt/version.rb +3 -3
  40. data/ruby-jwt.gemspec +6 -8
  41. metadata +39 -95
  42. data/.codeclimate.yml +0 -20
  43. data/.travis.yml +0 -13
  44. data/Manifest +0 -8
  45. data/spec/fixtures/certs/ec256-private.pem +0 -8
  46. data/spec/fixtures/certs/ec256-public.pem +0 -4
  47. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  48. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  49. data/spec/fixtures/certs/ec384-private.pem +0 -9
  50. data/spec/fixtures/certs/ec384-public.pem +0 -5
  51. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  52. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  53. data/spec/fixtures/certs/ec512-private.pem +0 -10
  54. data/spec/fixtures/certs/ec512-public.pem +0 -6
  55. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  56. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  57. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  58. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  59. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  60. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  61. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  62. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  63. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  64. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  65. data/spec/integration/readme_examples_spec.rb +0 -190
  66. data/spec/jwt/verify_spec.rb +0 -197
  67. data/spec/jwt_spec.rb +0 -240
  68. data/spec/spec_helper.rb +0 -31
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
6
+ KTY = 'oct'.freeze
7
+ KTYS = [KTY, String].freeze
8
+
9
+ def initialize(keypair, kid = nil)
10
+ raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
11
+
12
+ super
13
+ @kid = kid || generate_kid
14
+ end
15
+
16
+ def private?
17
+ true
18
+ end
19
+
20
+ def public_key
21
+ nil
22
+ end
23
+
24
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
25
+ def export(options = {})
26
+ exported_hash = {
27
+ kty: KTY,
28
+ kid: kid
29
+ }
30
+
31
+ return exported_hash unless private? && options[:include_private] == true
32
+
33
+ exported_hash.merge(
34
+ k: keypair
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def generate_kid
41
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
42
+ OpenSSL::ASN1::UTF8String.new(KTY)])
43
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
44
+ end
45
+
46
+ class << self
47
+ def import(jwk_data)
48
+ jwk_k = jwk_data[:k] || jwk_data['k']
49
+ jwk_kid = jwk_data[:kid] || jwk_data['kid']
50
+
51
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
52
+
53
+ self.new(jwk_k, jwk_kid)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KeyBase
6
+ attr_reader :keypair, :kid
7
+
8
+ def initialize(keypair, kid = nil)
9
+ @keypair = keypair
10
+ @kid = kid
11
+ end
12
+
13
+ def self.inherited(klass)
14
+ ::JWT::JWK.classes << klass
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KeyFinder
6
+ def initialize(options)
7
+ jwks_or_loader = options[:jwks]
8
+ @jwks = jwks_or_loader if jwks_or_loader.is_a?(Hash)
9
+ @jwk_loader = jwks_or_loader if jwks_or_loader.respond_to?(:call)
10
+ end
11
+
12
+ def key_for(kid)
13
+ raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
14
+
15
+ jwk = resolve_key(kid)
16
+
17
+ raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
18
+ raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
19
+
20
+ ::JWT::JWK.import(jwk).keypair
21
+ end
22
+
23
+ private
24
+
25
+ def resolve_key(kid)
26
+ jwk = find_key(kid)
27
+
28
+ return jwk if jwk
29
+
30
+ if reloadable?
31
+ load_keys(invalidate: true)
32
+ return find_key(kid)
33
+ end
34
+
35
+ nil
36
+ end
37
+
38
+ def jwks
39
+ return @jwks if @jwks
40
+
41
+ load_keys
42
+ @jwks
43
+ end
44
+
45
+ def load_keys(opts = {})
46
+ @jwks = @jwk_loader.call(opts)
47
+ end
48
+
49
+ def jwks_keys
50
+ Array(jwks[:keys] || jwks['keys'])
51
+ end
52
+
53
+ def find_key(kid)
54
+ jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
55
+ end
56
+
57
+ def reloadable?
58
+ @jwk_loader
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class RSA < KeyBase
6
+ BINARY = 2
7
+ KTY = 'RSA'.freeze
8
+ KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
+ RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
10
+
11
+ def initialize(keypair, kid = nil)
12
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
+ super(keypair, kid || generate_kid(keypair.public_key))
14
+ end
15
+
16
+ def private?
17
+ keypair.private?
18
+ end
19
+
20
+ def public_key
21
+ keypair.public_key
22
+ end
23
+
24
+ def export(options = {})
25
+ exported_hash = {
26
+ kty: KTY,
27
+ n: encode_open_ssl_bn(public_key.n),
28
+ e: encode_open_ssl_bn(public_key.e),
29
+ kid: kid
30
+ }
31
+
32
+ return exported_hash unless private? && options[:include_private] == true
33
+
34
+ append_private_parts(exported_hash)
35
+ end
36
+
37
+ private
38
+
39
+ def generate_kid(public_key)
40
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
41
+ OpenSSL::ASN1::Integer.new(public_key.e)])
42
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
43
+ end
44
+
45
+ def append_private_parts(the_hash)
46
+ the_hash.merge(
47
+ d: encode_open_ssl_bn(keypair.d),
48
+ p: encode_open_ssl_bn(keypair.p),
49
+ q: encode_open_ssl_bn(keypair.q),
50
+ dp: encode_open_ssl_bn(keypair.dmp1),
51
+ dq: encode_open_ssl_bn(keypair.dmq1),
52
+ qi: encode_open_ssl_bn(keypair.iqmp)
53
+ )
54
+ end
55
+
56
+ def encode_open_ssl_bn(key_part)
57
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
58
+ end
59
+
60
+ class << self
61
+ def import(jwk_data)
62
+ pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
63
+ decode_open_ssl_bn(value)
64
+ end
65
+ kid = jwk_attributes(jwk_data, :kid)[:kid]
66
+ self.new(rsa_pkey(pkey_params), kid)
67
+ end
68
+
69
+ private
70
+
71
+ def jwk_attributes(jwk_data, *attributes)
72
+ attributes.each_with_object({}) do |attribute, hash|
73
+ value = jwk_data[attribute] || jwk_data[attribute.to_s]
74
+ value = yield(value) if block_given?
75
+ hash[attribute] = value
76
+ end
77
+ end
78
+
79
+ def rsa_pkey(rsa_parameters)
80
+ raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
81
+
82
+ populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
83
+ end
84
+
85
+ if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
86
+ def populate_key(rsa_key, rsa_parameters)
87
+ rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
88
+ rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
89
+ rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
90
+ rsa_key
91
+ end
92
+ else
93
+ def populate_key(rsa_key, rsa_parameters)
94
+ rsa_key.n = rsa_parameters[:n]
95
+ rsa_key.e = rsa_parameters[:e]
96
+ rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
97
+ rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
98
+ rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
99
+ rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
100
+ rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
101
+ rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
102
+
103
+ rsa_key
104
+ end
105
+ end
106
+
107
+ def decode_open_ssl_bn(jwk_data)
108
+ return nil unless jwk_data
109
+
110
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,57 @@
1
+ module JWT
2
+ # Collection of security methods
3
+ #
4
+ # @see: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/security_utils.rb
5
+ module SecurityUtils
6
+ module_function
7
+
8
+ def secure_compare(left, right)
9
+ left_bytesize = left.bytesize
10
+
11
+ return false unless left_bytesize == right.bytesize
12
+
13
+ unpacked_left = left.unpack "C#{left_bytesize}"
14
+ result = 0
15
+ right.each_byte { |byte| result |= byte ^ unpacked_left.shift }
16
+ result.zero?
17
+ end
18
+
19
+ def verify_rsa(algorithm, public_key, signing_input, signature)
20
+ public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
21
+ end
22
+
23
+ def verify_ps(algorithm, public_key, signing_input, signature)
24
+ formatted_algorithm = algorithm.sub('PS', 'sha')
25
+
26
+ public_key.verify_pss(formatted_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: formatted_algorithm)
27
+ end
28
+
29
+ def asn1_to_raw(signature, public_key)
30
+ byte_size = (public_key.group.degree + 7) / 8
31
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
32
+ end
33
+
34
+ def raw_to_asn1(signature, private_key)
35
+ byte_size = (private_key.group.degree + 7) / 8
36
+ sig_bytes = signature[0..(byte_size - 1)]
37
+ sig_char = signature[byte_size..-1] || ''
38
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
39
+ end
40
+
41
+ def rbnacl_fixup(algorithm, key)
42
+ algorithm = algorithm.sub('HS', 'SHA').to_sym
43
+
44
+ return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
45
+
46
+ authenticator = RbNaCl::HMAC.const_get(algorithm)
47
+
48
+ # Fall back to OpenSSL for keys larger than 32 bytes.
49
+ return [] if key.bytesize > authenticator.key_bytes
50
+
51
+ [
52
+ authenticator,
53
+ key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
54
+ ]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/security_utils'
4
+ require 'openssl'
5
+ require 'jwt/algos'
6
+ begin
7
+ require 'rbnacl'
8
+ rescue LoadError
9
+ raise if defined?(RbNaCl)
10
+ end
11
+
12
+ # JWT::Signature module
13
+ module JWT
14
+ # Signature logic for JWT
15
+ module Signature
16
+ extend self
17
+ ToSign = Struct.new(:algorithm, :msg, :key)
18
+ ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
19
+
20
+ def sign(algorithm, msg, key)
21
+ algo, code = Algos.find(algorithm)
22
+ algo.sign ToSign.new(code, msg, key)
23
+ end
24
+
25
+ def verify(algorithm, key, signing_input, signature)
26
+ return true if algorithm.casecmp('none').zero?
27
+
28
+ raise JWT::DecodeError, 'No verification key available' unless key
29
+
30
+ algo, code = Algos.find(algorithm)
31
+ verified = algo.verify(ToVerify.new(code, key, signing_input, signature))
32
+ raise(JWT::VerificationError, 'Signature verification raised') unless verified
33
+ rescue OpenSSL::PKey::PKeyError
34
+ raise JWT::VerificationError, 'Signature verification raised'
35
+ ensure
36
+ OpenSSL.errors.clear
37
+ end
38
+ end
39
+ end
data/lib/jwt/verify.rb CHANGED
@@ -1,106 +1,98 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'jwt/error'
3
4
 
4
5
  module JWT
5
6
  # JWT verify methods
6
7
  class Verify
8
+ DEFAULTS = {
9
+ leeway: 0
10
+ }.freeze
11
+
7
12
  class << self
8
- %w(verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub).each do |method_name|
13
+ %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method_name|
9
14
  define_method method_name do |payload, options|
10
15
  new(payload, options).send(method_name)
11
16
  end
12
17
  end
18
+
19
+ def verify_claims(payload, options)
20
+ options.each do |key, val|
21
+ next unless key.to_s =~ /verify/
22
+ Verify.send(key, payload, options) if val
23
+ end
24
+ end
13
25
  end
14
26
 
15
27
  def initialize(payload, options)
16
28
  @payload = payload
17
- @options = options
29
+ @options = DEFAULTS.merge(options)
18
30
  end
19
31
 
20
32
  def verify_aud
21
- return unless (options_aud = extract_option(:aud))
22
-
23
- if @payload['aud'].is_a?(Array)
24
- verify_aud_array(@payload['aud'], options_aud)
25
- else
26
- raise(
27
- JWT::InvalidAudError,
28
- "Invalid audience. Expected #{options_aud}, received #{@payload['aud'] || '<none>'}"
29
- ) unless @payload['aud'].to_s == options_aud.to_s
30
- end
31
- end
33
+ return unless (options_aud = @options[:aud])
32
34
 
33
- def verify_aud_array(audience, options_aud)
34
- if options_aud.is_a?(Array)
35
- options_aud.each do |aud|
36
- raise(JWT::InvalidAudError, 'Invalid audience') unless audience.include?(aud.to_s)
37
- end
38
- else
39
- raise(JWT::InvalidAudError, 'Invalid audience') unless audience.include?(options_aud.to_s)
40
- end
35
+ aud = @payload['aud']
36
+ raise(JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{aud || '<none>'}") if ([*aud] & [*options_aud]).empty?
41
37
  end
42
38
 
43
39
  def verify_expiration
44
40
  return unless @payload.include?('exp')
45
-
46
- if @payload['exp'].to_i <= (Time.now.to_i - leeway)
47
- raise(JWT::ExpiredSignature, 'Signature has expired')
48
- end
41
+ raise(JWT::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
49
42
  end
50
43
 
51
44
  def verify_iat
52
45
  return unless @payload.include?('iat')
53
46
 
54
- if !@payload['iat'].is_a?(Numeric) || @payload['iat'].to_f > (Time.now.to_f + leeway)
55
- raise(JWT::InvalidIatError, 'Invalid iat')
56
- end
47
+ iat = @payload['iat']
48
+ raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > Time.now.to_f
57
49
  end
58
50
 
59
51
  def verify_iss
60
- return unless (options_iss = extract_option(:iss))
52
+ return unless (options_iss = @options[:iss])
61
53
 
62
- if @payload['iss'].to_s != options_iss.to_s
63
- raise(
64
- JWT::InvalidIssuerError,
65
- "Invalid issuer. Expected #{options_iss}, received #{@payload['iss'] || '<none>'}"
66
- )
67
- end
54
+ iss = @payload['iss']
55
+
56
+ return if Array(options_iss).map(&:to_s).include?(iss.to_s)
57
+
58
+ raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
68
59
  end
69
60
 
70
61
  def verify_jti
71
- options_verify_jti = extract_option(:verify_jti)
62
+ options_verify_jti = @options[:verify_jti]
63
+ jti = @payload['jti']
64
+
72
65
  if options_verify_jti.respond_to?(:call)
73
- raise(JWT::InvalidJtiError, 'Invalid jti') unless options_verify_jti.call(@payload['jti'])
74
- else
75
- raise(JWT::InvalidJtiError, 'Missing jti') if @payload['jti'].to_s.strip.empty?
66
+ verified = options_verify_jti.arity == 2 ? options_verify_jti.call(jti, @payload) : options_verify_jti.call(jti)
67
+ raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
68
+ elsif jti.to_s.strip.empty?
69
+ raise(JWT::InvalidJtiError, 'Missing jti')
76
70
  end
77
71
  end
78
72
 
79
73
  def verify_not_before
80
74
  return unless @payload.include?('nbf')
81
-
82
- if @payload['nbf'].to_i > (Time.now.to_i + leeway)
83
- raise(JWT::ImmatureSignature, 'Signature nbf has not been reached')
84
- end
75
+ raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
85
76
  end
86
77
 
87
78
  def verify_sub
88
- return unless (options_sub = extract_option(:sub))
89
-
90
- raise(
91
- JWT::InvalidSubError,
92
- "Invalid subject. Expected #{options_sub}, received #{@payload['sub'] || '<none>'}"
93
- ) unless @payload['sub'].to_s == options_sub.to_s
79
+ return unless (options_sub = @options[:sub])
80
+ sub = @payload['sub']
81
+ raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
94
82
  end
95
83
 
96
84
  private
97
85
 
98
- def extract_option(key)
99
- @options.values_at(key.to_sym, key.to_s).compact.first
86
+ def global_leeway
87
+ @options[:leeway]
88
+ end
89
+
90
+ def exp_leeway
91
+ @options[:exp_leeway] || global_leeway
100
92
  end
101
93
 
102
- def leeway
103
- extract_option :leeway
94
+ def nbf_leeway
95
+ @options[:nbf_leeway] || global_leeway
104
96
  end
105
97
  end
106
98
  end