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
data/Rakefile CHANGED
@@ -1,11 +1,14 @@
1
+ require 'bundler/setup'
1
2
  require 'bundler/gem_tasks'
2
3
 
3
4
  begin
4
5
  require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
5
7
 
6
8
  RSpec::Core::RakeTask.new(:test)
9
+ RuboCop::RakeTask.new(:rubocop)
7
10
 
8
- task default: :test
11
+ task default: %i[rubocop test]
9
12
  rescue LoadError
10
13
  puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
11
14
  end
data/lib/jwt.rb CHANGED
@@ -1,192 +1,30 @@
1
1
  # frozen_string_literal: true
2
- require 'base64'
3
- require 'openssl'
2
+
3
+ require 'jwt/base64'
4
+ require 'jwt/json'
4
5
  require 'jwt/decode'
6
+ require 'jwt/default_options'
7
+ require 'jwt/encode'
5
8
  require 'jwt/error'
6
- require 'jwt/json'
9
+ require 'jwt/jwk'
7
10
 
8
11
  # JSON Web Token implementation
9
12
  #
10
13
  # Should be up to date with the latest spec:
11
- # https://tools.ietf.org/html/rfc7519#section-4.1.5
14
+ # https://tools.ietf.org/html/rfc7519
12
15
  module JWT
13
- extend JWT::Json
14
-
15
- NAMED_CURVES = {
16
- 'prime256v1' => 'ES256',
17
- 'secp384r1' => 'ES384',
18
- 'secp521r1' => 'ES512'
19
- }.freeze
20
-
21
- DEFAULT_OPTIONS = {
22
- verify_expiration: true,
23
- verify_not_before: true,
24
- verify_iss: false,
25
- verify_iat: false,
26
- verify_jti: false,
27
- verify_aud: false,
28
- verify_sub: false,
29
- leeway: 0
30
- }.freeze
16
+ include JWT::DefaultOptions
31
17
 
32
18
  module_function
33
19
 
34
- def sign(algorithm, msg, key)
35
- if %w(HS256 HS384 HS512).include?(algorithm)
36
- sign_hmac(algorithm, msg, key)
37
- elsif %w(RS256 RS384 RS512).include?(algorithm)
38
- sign_rsa(algorithm, msg, key)
39
- elsif %w(ES256 ES384 ES512).include?(algorithm)
40
- sign_ecdsa(algorithm, msg, key)
41
- else
42
- raise NotImplementedError, 'Unsupported signing method'
43
- end
44
- end
45
-
46
- def sign_rsa(algorithm, msg, private_key)
47
- private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
48
- end
49
-
50
- def sign_ecdsa(algorithm, msg, private_key)
51
- key_algorithm = NAMED_CURVES[private_key.group.curve_name]
52
- if algorithm != key_algorithm
53
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
54
- end
55
-
56
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
57
- asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
58
- end
59
-
60
- def verify_rsa(algorithm, public_key, signing_input, signature)
61
- public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
62
- end
63
-
64
- def verify_ecdsa(algorithm, public_key, signing_input, signature)
65
- key_algorithm = NAMED_CURVES[public_key.group.curve_name]
66
- if algorithm != key_algorithm
67
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
68
- end
69
-
70
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
71
- public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
72
- end
73
-
74
- def sign_hmac(algorithm, msg, key)
75
- OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
76
- end
77
-
78
- def base64url_encode(str)
79
- Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
80
- end
81
-
82
- def encoded_header(algorithm = 'HS256', header_fields = {})
83
- header = { 'typ' => 'JWT', 'alg' => algorithm }.merge(header_fields)
84
- base64url_encode(encode_json(header))
85
- end
86
-
87
- def encoded_payload(payload)
88
- raise InvalidPayload, 'exp claim must be an integer' if payload['exp'] && payload['exp'].is_a?(Time)
89
- base64url_encode(encode_json(payload))
90
- end
91
-
92
- def encoded_signature(signing_input, key, algorithm)
93
- if algorithm == 'none'
94
- ''
95
- else
96
- signature = sign(algorithm, signing_input, key)
97
- base64url_encode(signature)
98
- end
99
- end
100
-
101
20
  def encode(payload, key, algorithm = 'HS256', header_fields = {})
102
- algorithm ||= 'none'
103
- segments = []
104
- segments << encoded_header(algorithm, header_fields)
105
- segments << encoded_payload(payload)
106
- segments << encoded_signature(segments.join('.'), key, algorithm)
107
- segments.join('.')
108
- end
109
-
110
- def decoded_segments(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
111
- raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
112
-
113
- merged_options = DEFAULT_OPTIONS.merge(custom_options)
114
-
115
- decoder = Decode.new jwt, key, verify, merged_options, &keyfinder
116
- decoder.decode_segments
117
- end
118
-
119
- def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
120
- raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
121
-
122
- merged_options = DEFAULT_OPTIONS.merge(custom_options)
123
- decoder = Decode.new jwt, key, verify, merged_options, &keyfinder
124
- header, payload, signature, signing_input = decoder.decode_segments
125
- decode_verify_signature(key, header, signature, signing_input, merged_options, &keyfinder) if verify
126
- decoder.verify
127
-
128
- raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
129
-
130
- [payload, header]
131
- end
132
-
133
- def decode_verify_signature(key, header, signature, signing_input, options, &keyfinder)
134
- algo, key = signature_algorithm_and_key(header, key, &keyfinder)
135
- if options[:algorithm] && algo != options[:algorithm]
136
- raise JWT::IncorrectAlgorithm, 'Expected a different algorithm'
137
- end
138
- verify_signature(algo, key, signing_input, signature)
139
- end
140
-
141
- def signature_algorithm_and_key(header, key, &keyfinder)
142
- key = yield(header) if keyfinder
143
- [header['alg'], key]
144
- end
145
-
146
- def verify_signature(algo, key, signing_input, signature)
147
- verify_signature_algo(algo, key, signing_input, signature)
148
- rescue OpenSSL::PKey::PKeyError
149
- raise JWT::VerificationError, 'Signature verification raised'
150
- ensure
151
- OpenSSL.errors.clear
152
- end
153
-
154
- def verify_signature_algo(algo, key, signing_input, signature)
155
- if %w(HS256 HS384 HS512).include?(algo)
156
- raise(JWT::VerificationError, 'Signature verification raised') unless secure_compare(signature, sign_hmac(algo, signing_input, key))
157
- elsif %w(RS256 RS384 RS512).include?(algo)
158
- raise(JWT::VerificationError, 'Signature verification raised') unless verify_rsa(algo, key, signing_input, signature)
159
- elsif %w(ES256 ES384 ES512).include?(algo)
160
- raise(JWT::VerificationError, 'Signature verification raised') unless verify_ecdsa(algo, key, signing_input, signature)
161
- else
162
- raise JWT::VerificationError, 'Algorithm not supported'
163
- end
164
- end
165
-
166
- # From devise
167
- # constant-time comparison algorithm to prevent timing attacks
168
- def secure_compare(a, b)
169
- return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
170
- l = a.unpack "C#{a.bytesize}"
171
-
172
- res = 0
173
- b.each_byte { |byte| res |= byte ^ l.shift }
174
- res.zero?
175
- end
176
-
177
- def raw_to_asn1(signature, private_key)
178
- byte_size = (private_key.group.degree + 7) / 8
179
- r = signature[0..(byte_size - 1)]
180
- s = signature[byte_size..-1]
181
- OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
182
- end
183
-
184
- def asn1_to_raw(signature, public_key)
185
- byte_size = (public_key.group.degree + 7) / 8
186
- OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
21
+ Encode.new(payload: payload,
22
+ key: key,
23
+ algorithm: algorithm,
24
+ headers: header_fields).segments
187
25
  end
188
26
 
189
- def base64url_decode(str)
190
- Decode.base64url_decode(str)
27
+ def decode(jwt, key = nil, verify = true, options = {}, &keyfinder)
28
+ Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments
191
29
  end
192
30
  end
data/lib/jwt/algos.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/algos/hmac'
4
+ require 'jwt/algos/eddsa'
5
+ require 'jwt/algos/ecdsa'
6
+ require 'jwt/algos/rsa'
7
+ require 'jwt/algos/ps'
8
+ require 'jwt/algos/none'
9
+ require 'jwt/algos/unsupported'
10
+
11
+ # JWT::Signature module
12
+ module JWT
13
+ # Signature logic for JWT
14
+ module Algos
15
+ extend self
16
+
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
26
+
27
+ def find(algorithm)
28
+ indexed[algorithm && algorithm.downcase]
29
+ end
30
+
31
+ private
32
+
33
+ def indexed
34
+ @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]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module JWT
2
+ module Algos
3
+ module Ecdsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[ES256 ES384 ES512].freeze
7
+ NAMED_CURVES = {
8
+ 'prime256v1' => 'ES256',
9
+ 'secp384r1' => 'ES384',
10
+ 'secp521r1' => 'ES512'
11
+ }.freeze
12
+
13
+ def sign(to_sign)
14
+ algorithm, msg, key = to_sign.values
15
+ key_algorithm = NAMED_CURVES[key.group.curve_name]
16
+ if algorithm != key_algorithm
17
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
18
+ end
19
+
20
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
21
+ SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
22
+ end
23
+
24
+ def verify(to_verify)
25
+ algorithm, public_key, signing_input, signature = to_verify.values
26
+ key_algorithm = NAMED_CURVES[public_key.group.curve_name]
27
+ if algorithm != key_algorithm
28
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
29
+ end
30
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
31
+ public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ module JWT
2
+ module Algos
3
+ module Eddsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[ED25519].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided" if algorithm.downcase.to_sym != key.primitive
12
+ key.sign(msg)
13
+ end
14
+
15
+ def verify(to_verify)
16
+ algorithm, public_key, signing_input, signature = to_verify.values
17
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive
18
+ raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
19
+ public_key.verify(signature, signing_input)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module JWT
2
+ module Algos
3
+ module Hmac
4
+ module_function
5
+
6
+ SUPPORTED = %w[HS256 HS512256 HS384 HS512].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ key ||= ''
11
+ authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
12
+ if authenticator && padded_key
13
+ authenticator.auth(padded_key, msg.encode('binary'))
14
+ else
15
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
16
+ end
17
+ end
18
+
19
+ def verify(to_verify)
20
+ algorithm, public_key, signing_input, signature = to_verify.values
21
+ authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
22
+ if authenticator && padded_key
23
+ begin
24
+ authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
25
+ rescue RbNaCl::BadAuthenticatorError
26
+ false
27
+ end
28
+ else
29
+ SecurityUtils.secure_compare(signature, sign(JWT::Signature::ToSign.new(algorithm, signing_input, public_key)))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+ module JWT
2
+ module Algos
3
+ module None
4
+ module_function
5
+
6
+ SUPPORTED = %w[none].freeze
7
+
8
+ def sign(*); end
9
+
10
+ def verify(*)
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ module JWT
2
+ module Algos
3
+ module Ps
4
+ # RSASSA-PSS signing algorithms
5
+
6
+ module_function
7
+
8
+ SUPPORTED = %w[PS256 PS384 PS512].freeze
9
+
10
+ def sign(to_sign)
11
+ require_openssl!
12
+
13
+ algorithm, msg, key = to_sign.values
14
+
15
+ key_class = key.class
16
+
17
+ raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String
18
+
19
+ translated_algorithm = algorithm.sub('PS', 'sha')
20
+
21
+ key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
22
+ end
23
+
24
+ def verify(to_verify)
25
+ require_openssl!
26
+
27
+ SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
28
+ end
29
+
30
+ def require_openssl!
31
+ if Object.const_defined?('OpenSSL')
32
+ major, minor = OpenSSL::VERSION.split('.').first(2)
33
+
34
+ unless major.to_i >= 2 && minor.to_i >= 1
35
+ raise JWT::RequiredDependencyError, "You currently have OpenSSL #{OpenSSL::VERSION}. PS support requires >= 2.1"
36
+ end
37
+ else
38
+ raise JWT::RequiredDependencyError, 'PS signing requires OpenSSL +2.1'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ module JWT
2
+ module Algos
3
+ module Rsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[RS256 RS384 RS512].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.class == String
11
+ key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
12
+ end
13
+
14
+ def verify(to_verify)
15
+ SecurityUtils.verify_rsa(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
16
+ end
17
+ end
18
+ end
19
+ end