jwt 2.2.2 → 2.4.0.beta1

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.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
6
+ KTY = 'oct'
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,19 @@
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
+ super
15
+ ::JWT::JWK.classes << klass
16
+ end
17
+ end
18
+ end
19
+ end
@@ -14,6 +14,7 @@ module JWT
14
14
 
15
15
  jwk = resolve_key(kid)
16
16
 
17
+ raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
17
18
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
18
19
 
19
20
  ::JWT::JWK.import(jwk).keypair
@@ -45,8 +46,12 @@ module JWT
45
46
  @jwks = @jwk_loader.call(opts)
46
47
  end
47
48
 
49
+ def jwks_keys
50
+ Array(jwks[:keys] || jwks['keys'])
51
+ end
52
+
48
53
  def find_key(kid)
49
- Array(jwks[:keys]).find { |key| key[:kid] == kid }
54
+ jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
50
55
  end
51
56
 
52
57
  def reloadable?
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  module JWT
4
4
  module JWK
5
- class RSA
6
- attr_reader :keypair
7
-
5
+ class RSA < KeyBase
8
6
  BINARY = 2
9
- KTY = 'RSA'.freeze
7
+ KTY = 'RSA'
8
+ KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
+ RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
10
10
 
11
- def initialize(keypair)
11
+ def initialize(keypair, kid = nil)
12
12
  raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
13
 
14
- @keypair = keypair
14
+ super(keypair, kid || generate_kid(keypair.public_key))
15
15
  end
16
16
 
17
17
  def private?
@@ -22,32 +22,94 @@ module JWT
22
22
  keypair.public_key
23
23
  end
24
24
 
25
- def kid
25
+ def export(options = {})
26
+ exported_hash = {
27
+ kty: KTY,
28
+ n: encode_open_ssl_bn(public_key.n),
29
+ e: encode_open_ssl_bn(public_key.e),
30
+ kid: kid
31
+ }
32
+
33
+ return exported_hash unless private? && options[:include_private] == true
34
+
35
+ append_private_parts(exported_hash)
36
+ end
37
+
38
+ private
39
+
40
+ def generate_kid(public_key)
26
41
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
27
42
  OpenSSL::ASN1::Integer.new(public_key.e)])
28
43
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
29
44
  end
30
45
 
31
- def export
32
- {
33
- kty: KTY,
34
- n: ::Base64.urlsafe_encode64(public_key.n.to_s(BINARY), padding: false),
35
- e: ::Base64.urlsafe_encode64(public_key.e.to_s(BINARY), padding: false),
36
- kid: kid
37
- }
46
+ def append_private_parts(the_hash)
47
+ the_hash.merge(
48
+ d: encode_open_ssl_bn(keypair.d),
49
+ p: encode_open_ssl_bn(keypair.p),
50
+ q: encode_open_ssl_bn(keypair.q),
51
+ dp: encode_open_ssl_bn(keypair.dmp1),
52
+ dq: encode_open_ssl_bn(keypair.dmq1),
53
+ qi: encode_open_ssl_bn(keypair.iqmp)
54
+ )
55
+ end
56
+
57
+ def encode_open_ssl_bn(key_part)
58
+ Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
38
59
  end
39
60
 
40
- def self.import(jwk_data)
41
- imported_key = OpenSSL::PKey::RSA.new
42
- if imported_key.respond_to?(:set_key)
43
- imported_key.set_key(OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY),
44
- OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY),
45
- nil)
61
+ class << self
62
+ def import(jwk_data)
63
+ pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
64
+ decode_open_ssl_bn(value)
65
+ end
66
+ kid = jwk_attributes(jwk_data, :kid)[:kid]
67
+ self.new(rsa_pkey(pkey_params), kid)
68
+ end
69
+
70
+ private
71
+
72
+ def jwk_attributes(jwk_data, *attributes)
73
+ attributes.each_with_object({}) do |attribute, hash|
74
+ value = jwk_data[attribute] || jwk_data[attribute.to_s]
75
+ value = yield(value) if block_given?
76
+ hash[attribute] = value
77
+ end
78
+ end
79
+
80
+ def rsa_pkey(rsa_parameters)
81
+ raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
82
+
83
+ populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
84
+ end
85
+
86
+ if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
87
+ def populate_key(rsa_key, rsa_parameters)
88
+ rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
89
+ rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
90
+ rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
91
+ rsa_key
92
+ end
46
93
  else
47
- imported_key.n = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY)
48
- imported_key.e = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY)
94
+ def populate_key(rsa_key, rsa_parameters)
95
+ rsa_key.n = rsa_parameters[:n]
96
+ rsa_key.e = rsa_parameters[:e]
97
+ rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
98
+ rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
99
+ rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
100
+ rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
101
+ rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
102
+ rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
103
+
104
+ rsa_key
105
+ end
106
+ end
107
+
108
+ def decode_open_ssl_bn(jwk_data)
109
+ return nil unless jwk_data
110
+
111
+ OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
49
112
  end
50
- self.new(imported_key)
51
113
  end
52
114
  end
53
115
  end
data/lib/jwt/jwk.rb CHANGED
@@ -1,31 +1,52 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'jwk/rsa'
4
3
  require_relative 'jwk/key_finder'
5
4
 
6
5
  module JWT
7
6
  module JWK
8
- MAPPINGS = {
9
- 'RSA' => ::JWT::JWK::RSA,
10
- OpenSSL::PKey::RSA => ::JWT::JWK::RSA
11
- }.freeze
12
-
13
7
  class << self
14
8
  def import(jwk_data)
15
- raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_data[:kty]
9
+ jwk_kty = jwk_data[:kty] || jwk_data['kty']
10
+ raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
16
11
 
17
- MAPPINGS.fetch(jwk_data[:kty].to_s) do |kty|
12
+ mappings.fetch(jwk_kty.to_s) do |kty|
18
13
  raise JWT::JWKError, "Key type #{kty} not supported"
19
14
  end.import(jwk_data)
20
15
  end
21
16
 
22
- def create_from(keypair)
23
- MAPPINGS.fetch(keypair.class) do |klass|
17
+ def create_from(keypair, kid = nil)
18
+ mappings.fetch(keypair.class) do |klass|
24
19
  raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
25
- end.new(keypair)
20
+ end.new(keypair, kid)
21
+ end
22
+
23
+ def classes
24
+ @mappings = nil # reset the cached mappings
25
+ @classes ||= []
26
26
  end
27
27
 
28
28
  alias new create_from
29
+
30
+ private
31
+
32
+ def mappings
33
+ @mappings ||= generate_mappings
34
+ end
35
+
36
+ def generate_mappings
37
+ classes.each_with_object({}) do |klass, hash|
38
+ next unless klass.const_defined?('KTYS')
39
+
40
+ Array(klass::KTYS).each do |kty|
41
+ hash[kty] = klass
42
+ end
43
+ end
44
+ end
29
45
  end
30
46
  end
31
47
  end
48
+
49
+ require_relative 'jwk/key_base'
50
+ require_relative 'jwk/ec'
51
+ require_relative 'jwk/rsa'
52
+ require_relative 'jwk/hmac'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  # Collection of security methods
3
5
  #
data/lib/jwt/signature.rb CHANGED
@@ -2,12 +2,7 @@
2
2
 
3
3
  require 'jwt/security_utils'
4
4
  require 'openssl'
5
- require 'jwt/algos/hmac'
6
- require 'jwt/algos/eddsa'
7
- require 'jwt/algos/ecdsa'
8
- require 'jwt/algos/rsa'
9
- require 'jwt/algos/ps'
10
- require 'jwt/algos/unsupported'
5
+ require 'jwt/algos'
11
6
  begin
12
7
  require 'rbnacl'
13
8
  rescue LoadError
@@ -18,33 +13,19 @@ end
18
13
  module JWT
19
14
  # Signature logic for JWT
20
15
  module Signature
21
- extend self
22
- ALGOS = [
23
- Algos::Hmac,
24
- Algos::Ecdsa,
25
- Algos::Rsa,
26
- Algos::Eddsa,
27
- Algos::Ps,
28
- Algos::Unsupported
29
- ].freeze
16
+ module_function
17
+
30
18
  ToSign = Struct.new(:algorithm, :msg, :key)
31
19
  ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
32
20
 
33
21
  def sign(algorithm, msg, key)
34
- algo = ALGOS.find do |alg|
35
- alg.const_get(:SUPPORTED).include? algorithm
36
- end
37
- algo.sign ToSign.new(algorithm, msg, key)
22
+ algo, code = Algos.find(algorithm)
23
+ algo.sign ToSign.new(code, msg, key)
38
24
  end
39
25
 
40
26
  def verify(algorithm, key, signing_input, signature)
41
- raise JWT::DecodeError, 'No verification key available' unless key
42
-
43
- algo = ALGOS.find do |alg|
44
- alg.const_get(:SUPPORTED).include? algorithm
45
- end
46
- verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature))
47
- raise(JWT::VerificationError, 'Signature verification raised') unless verified
27
+ algo, code = Algos.find(algorithm)
28
+ algo.verify(ToVerify.new(code, key, signing_input, signature))
48
29
  rescue OpenSSL::PKey::PKeyError
49
30
  raise JWT::VerificationError, 'Signature verification raised'
50
31
  ensure
data/lib/jwt/verify.rb CHANGED
@@ -10,7 +10,7 @@ module JWT
10
10
  }.freeze
11
11
 
12
12
  class << self
13
- %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 verify_required_claims].each do |method_name|
14
14
  define_method method_name do |payload, options|
15
15
  new(payload, options).send(method_name)
16
16
  end
@@ -19,6 +19,7 @@ module JWT
19
19
  def verify_claims(payload, options)
20
20
  options.each do |key, val|
21
21
  next unless key.to_s =~ /verify/
22
+
22
23
  Verify.send(key, payload, options) if val
23
24
  end
24
25
  end
@@ -53,9 +54,14 @@ module JWT
53
54
 
54
55
  iss = @payload['iss']
55
56
 
56
- return if Array(options_iss).map(&:to_s).include?(iss.to_s)
57
+ options_iss = Array(options_iss).map { |item| item.is_a?(Symbol) ? item.to_s : item }
57
58
 
58
- raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
59
+ case iss
60
+ when *options_iss
61
+ nil
62
+ else
63
+ raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
64
+ end
59
65
  end
60
66
 
61
67
  def verify_jti
@@ -77,10 +83,19 @@ module JWT
77
83
 
78
84
  def verify_sub
79
85
  return unless (options_sub = @options[:sub])
86
+
80
87
  sub = @payload['sub']
81
88
  raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
82
89
  end
83
90
 
91
+ def verify_required_claims
92
+ return unless (options_required_claims = @options[:required_claims])
93
+
94
+ options_required_claims.each do |required_claim|
95
+ raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
96
+ end
97
+ end
98
+
84
99
  private
85
100
 
86
101
  def global_leeway
data/lib/jwt/version.rb CHANGED
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # Moments version builder module
@@ -12,11 +11,11 @@ module JWT
12
11
  # major version
13
12
  MAJOR = 2
14
13
  # minor version
15
- MINOR = 2
14
+ MINOR = 4
16
15
  # tiny version
17
- TINY = 2
16
+ TINY = 0
18
17
  # alpha, beta, etc. tag
19
- PRE = nil
18
+ PRE = 'beta1'
20
19
 
21
20
  # Build version string
22
21
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'jwt/error'
5
+
6
+ module JWT
7
+ # If the x5c header certificate chain can be validated by trusted root
8
+ # certificates, and none of the certificates are revoked, returns the public
9
+ # key from the first certificate.
10
+ # See https://tools.ietf.org/html/rfc7515#section-4.1.6
11
+ class X5cKeyFinder
12
+ def initialize(root_certificates, crls = nil)
13
+ raise(ArgumentError, 'Root certificates must be specified') unless root_certificates
14
+
15
+ @store = build_store(root_certificates, crls)
16
+ end
17
+
18
+ def from(x5c_header_or_certificates)
19
+ signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates)
20
+ store_context = OpenSSL::X509::StoreContext.new(@store, signing_certificate, certificate_chain)
21
+
22
+ if store_context.verify
23
+ signing_certificate.public_key
24
+ else
25
+ error = "Certificate verification failed: #{store_context.error_string}."
26
+ if (current_cert = store_context.current_cert)
27
+ error = "#{error} Certificate subject: #{current_cert.subject}."
28
+ end
29
+
30
+ raise(JWT::VerificationError, error)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_store(root_certificates, crls)
37
+ store = OpenSSL::X509::Store.new
38
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
39
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
40
+ root_certificates.each { |certificate| store.add_cert(certificate) }
41
+ crls&.each { |crl| store.add_crl(crl) }
42
+ store
43
+ end
44
+
45
+ def parse_certificates(x5c_header_or_certificates)
46
+ if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) }
47
+ x5c_header_or_certificates
48
+ else
49
+ x5c_header_or_certificates.map do |encoded|
50
+ OpenSSL::X509::Certificate.new(::Base64.strict_decode64(encoded))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/jwt.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jwt/base64'
3
+ require 'base64'
4
4
  require 'jwt/json'
5
5
  require 'jwt/decode'
6
6
  require 'jwt/default_options'
data/ruby-jwt.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- lib = File.expand_path('../lib/', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'jwt/version'
4
6
 
@@ -13,7 +15,11 @@ Gem::Specification.new do |spec|
13
15
  spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.'
14
16
  spec.homepage = 'https://github.com/jwt/ruby-jwt'
15
17
  spec.license = 'MIT'
16
- spec.required_ruby_version = '>= 2.1'
18
+ spec.required_ruby_version = '>= 2.5'
19
+ spec.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues',
21
+ 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md"
22
+ }
17
23
 
18
24
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) }
19
25
  spec.executables = []
@@ -24,11 +30,5 @@ Gem::Specification.new do |spec|
24
30
  spec.add_development_dependency 'bundler'
25
31
  spec.add_development_dependency 'rake'
26
32
  spec.add_development_dependency 'rspec'
27
- spec.add_development_dependency 'simplecov', '< 0.18'
28
- spec.add_development_dependency 'simplecov-json'
29
- spec.add_development_dependency 'codeclimate-test-reporter'
30
- spec.add_development_dependency 'codacy-coverage'
31
- spec.add_development_dependency 'rbnacl'
32
- # RSASSA-PSS support provided by OpenSSL +2.1
33
- spec.add_development_dependency 'openssl', '~> 2.1'
33
+ spec.add_development_dependency 'simplecov'
34
34
  end