jwt 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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,43 +2,113 @@
2
2
 
3
3
  module JWT
4
4
  module JWK
5
- class RSA
6
- extend Forwardable
7
-
8
- attr_reader :keypair
9
-
10
- def_delegators :keypair, :private?, :public_key
11
-
5
+ class RSA < KeyBase
12
6
  BINARY = 2
13
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
14
10
 
15
- def initialize(keypair)
11
+ def initialize(keypair, kid = nil)
16
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
17
15
 
18
- @keypair = keypair
16
+ def private?
17
+ keypair.private?
19
18
  end
20
19
 
21
- def kid
22
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
23
- OpenSSL::ASN1::Integer.new(public_key.e)])
24
- OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
20
+ def public_key
21
+ keypair.public_key
25
22
  end
26
23
 
27
- def export
28
- {
24
+ def export(options = {})
25
+ exported_hash = {
29
26
  kty: KTY,
30
- n: ::Base64.urlsafe_encode64(public_key.n.to_s(BINARY), padding: false),
31
- e: ::Base64.urlsafe_encode64(public_key.e.to_s(BINARY), padding: false),
27
+ n: encode_open_ssl_bn(public_key.n),
28
+ e: encode_open_ssl_bn(public_key.e),
32
29
  kid: kid
33
30
  }
31
+
32
+ return exported_hash unless private? && options[:include_private] == true
33
+
34
+ append_private_parts(exported_hash)
34
35
  end
35
36
 
36
- def self.import(jwk_data)
37
- imported_key = OpenSSL::PKey::RSA.new
38
- imported_key.set_key(OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY),
39
- OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY),
40
- nil)
41
- self.new(imported_key)
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
42
112
  end
43
113
  end
44
114
  end
data/lib/jwt/jwk.rb CHANGED
@@ -1,31 +1,51 @@
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
+ Array(klass::KTYS).each do |kty|
40
+ hash[kty] = klass
41
+ end
42
+ end
43
+ end
29
44
  end
30
45
  end
31
46
  end
47
+
48
+ require_relative 'jwk/key_base'
49
+ require_relative 'jwk/ec'
50
+ require_relative 'jwk/rsa'
51
+ require_relative 'jwk/hmac'
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
@@ -19,29 +14,21 @@ module JWT
19
14
  # Signature logic for JWT
20
15
  module Signature
21
16
  extend self
22
- ALGOS = [
23
- Algos::Hmac,
24
- Algos::Ecdsa,
25
- Algos::Rsa,
26
- Algos::Eddsa,
27
- Algos::Ps,
28
- Algos::Unsupported
29
- ].freeze
30
17
  ToSign = Struct.new(:algorithm, :msg, :key)
31
18
  ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
32
19
 
33
20
  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)
21
+ algo, code = Algos.find(algorithm)
22
+ algo.sign ToSign.new(code, msg, key)
38
23
  end
39
24
 
40
25
  def verify(algorithm, key, signing_input, signature)
41
- algo = ALGOS.find do |alg|
42
- alg.const_get(:SUPPORTED).include? algorithm
43
- end
44
- verified = algo.verify(ToVerify.new(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))
45
32
  raise(JWT::VerificationError, 'Signature verification raised') unless verified
46
33
  rescue OpenSSL::PKey::PKeyError
47
34
  raise JWT::VerificationError, 'Signature verification raised'
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
@@ -81,6 +81,13 @@ module JWT
81
81
  raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
82
82
  end
83
83
 
84
+ def verify_required_claims
85
+ return unless (options_required_claims = @options[:required_claims])
86
+ options_required_claims.each do |required_claim|
87
+ raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
88
+ end
89
+ end
90
+
84
91
  private
85
92
 
86
93
  def global_leeway
data/lib/jwt/version.rb CHANGED
@@ -12,13 +12,13 @@ module JWT
12
12
  # major version
13
13
  MAJOR = 2
14
14
  # minor version
15
- MINOR = 2
15
+ MINOR = 3
16
16
  # tiny version
17
17
  TINY = 0
18
18
  # alpha, beta, etc. tag
19
19
  PRE = nil
20
20
 
21
21
  # Build version string
22
- STRING = [[MAJOR, MINOR, TINY].compact.join('.'), PRE].compact.join('-')
22
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
23
23
  end
24
24
  end
data/ruby-jwt.gemspec CHANGED
@@ -14,6 +14,10 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = 'https://github.com/jwt/ruby-jwt'
15
15
  spec.license = 'MIT'
16
16
  spec.required_ruby_version = '>= 2.1'
17
+ spec.metadata = {
18
+ 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues',
19
+ 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md"
20
+ }
17
21
 
18
22
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) }
19
23
  spec.executables = []
@@ -25,10 +29,4 @@ Gem::Specification.new do |spec|
25
29
  spec.add_development_dependency 'rake'
26
30
  spec.add_development_dependency 'rspec'
27
31
  spec.add_development_dependency 'simplecov'
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'
34
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rudat
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-23 00:00:00.000000000 Z
11
+ date: 2021-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -80,76 +80,6 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: simplecov-json
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- - !ruby/object:Gem::Dependency
98
- name: codeclimate-test-reporter
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: codacy-coverage
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: rbnacl
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: openssl
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '2.1'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: '2.1'
153
83
  description: A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT)
154
84
  standard.
155
85
  email: timrudat@gmail.com
@@ -157,12 +87,12 @@ executables: []
157
87
  extensions: []
158
88
  extra_rdoc_files: []
159
89
  files:
160
- - ".codeclimate.yml"
161
- - ".ebert.yml"
90
+ - ".github/workflows/test.yml"
162
91
  - ".gitignore"
163
92
  - ".rspec"
164
93
  - ".rubocop.yml"
165
- - ".travis.yml"
94
+ - ".rubocop_todo.yml"
95
+ - ".sourcelevel.yml"
166
96
  - AUTHORS
167
97
  - Appraisals
168
98
  - CHANGELOG.md
@@ -171,9 +101,11 @@ files:
171
101
  - README.md
172
102
  - Rakefile
173
103
  - lib/jwt.rb
104
+ - lib/jwt/algos.rb
174
105
  - lib/jwt/algos/ecdsa.rb
175
106
  - lib/jwt/algos/eddsa.rb
176
107
  - lib/jwt/algos/hmac.rb
108
+ - lib/jwt/algos/none.rb
177
109
  - lib/jwt/algos/ps.rb
178
110
  - lib/jwt/algos/rsa.rb
179
111
  - lib/jwt/algos/unsupported.rb
@@ -185,6 +117,9 @@ files:
185
117
  - lib/jwt/error.rb
186
118
  - lib/jwt/json.rb
187
119
  - lib/jwt/jwk.rb
120
+ - lib/jwt/jwk/ec.rb
121
+ - lib/jwt/jwk/hmac.rb
122
+ - lib/jwt/jwk/key_base.rb
188
123
  - lib/jwt/jwk/key_finder.rb
189
124
  - lib/jwt/jwk/rsa.rb
190
125
  - lib/jwt/security_utils.rb
@@ -195,8 +130,10 @@ files:
195
130
  homepage: https://github.com/jwt/ruby-jwt
196
131
  licenses:
197
132
  - MIT
198
- metadata: {}
199
- post_install_message:
133
+ metadata:
134
+ bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues
135
+ changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.3.0/CHANGELOG.md
136
+ post_install_message:
200
137
  rdoc_options: []
201
138
  require_paths:
202
139
  - lib
@@ -211,8 +148,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
148
  - !ruby/object:Gem::Version
212
149
  version: '0'
213
150
  requirements: []
214
- rubygems_version: 3.0.3
215
- signing_key:
151
+ rubygems_version: 3.2.19
152
+ signing_key:
216
153
  specification_version: 4
217
154
  summary: JSON Web Token implementation in Ruby
218
155
  test_files: []
data/.codeclimate.yml DELETED
@@ -1,20 +0,0 @@
1
- engines:
2
- rubocop:
3
- enabled: true
4
- golint:
5
- enabled: false
6
- gofmt:
7
- enabled: false
8
- eslint:
9
- enabled: false
10
- csslint:
11
- enabled: false
12
-
13
- ratings:
14
- paths:
15
- - lib/**
16
- - "**.rb"
17
-
18
- exclude_paths:
19
- - spec/**/*
20
- - vendor/**/*
data/.travis.yml DELETED
@@ -1,20 +0,0 @@
1
- sudo: required
2
- cache: bundler
3
- dist: trusty
4
- language: ruby
5
- rvm:
6
- - 2.3
7
- - 2.4
8
- - 2.5
9
- - 2.6
10
- gemfiles:
11
- - gemfiles/standalone.gemfile
12
- - gemfiles/rails_5.0.gemfile
13
- - gemfiles/rails_5.1.gemfile
14
- - gemfiles/rails_5.2.gemfile
15
- script: "bundle exec rspec && bundle exec codeclimate-test-reporter"
16
- before_install:
17
- - sudo add-apt-repository ppa:chris-lea/libsodium -y
18
- - sudo apt-get update -q
19
- - sudo apt-get install libsodium-dev -y
20
- - gem install bundler