jwt 1.5.6 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/.ebert.yml +18 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +96 -0
  5. data/.travis.yml +26 -10
  6. data/AUTHORS +84 -0
  7. data/Appraisals +18 -0
  8. data/CHANGELOG.md +296 -10
  9. data/Gemfile +0 -1
  10. data/README.md +182 -64
  11. data/lib/jwt.rb +14 -176
  12. data/lib/jwt/algos/ecdsa.rb +35 -0
  13. data/lib/jwt/algos/eddsa.rb +23 -0
  14. data/lib/jwt/algos/hmac.rb +34 -0
  15. data/lib/jwt/algos/ps.rb +43 -0
  16. data/lib/jwt/algos/rsa.rb +19 -0
  17. data/lib/jwt/algos/unsupported.rb +16 -0
  18. data/lib/jwt/base64.rb +19 -0
  19. data/lib/jwt/claims_validator.rb +33 -0
  20. data/lib/jwt/decode.rb +81 -31
  21. data/lib/jwt/default_options.rb +15 -0
  22. data/lib/jwt/encode.rb +68 -0
  23. data/lib/jwt/error.rb +6 -0
  24. data/lib/jwt/json.rb +10 -9
  25. data/lib/jwt/jwk.rb +31 -0
  26. data/lib/jwt/jwk/key_finder.rb +57 -0
  27. data/lib/jwt/jwk/rsa.rb +54 -0
  28. data/lib/jwt/security_utils.rb +57 -0
  29. data/lib/jwt/signature.rb +54 -0
  30. data/lib/jwt/verify.rb +45 -53
  31. data/lib/jwt/version.rb +3 -3
  32. data/ruby-jwt.gemspec +11 -7
  33. metadata +76 -67
  34. data/Manifest +0 -8
  35. data/spec/fixtures/certs/ec256-private.pem +0 -8
  36. data/spec/fixtures/certs/ec256-public.pem +0 -4
  37. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  38. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  39. data/spec/fixtures/certs/ec384-private.pem +0 -9
  40. data/spec/fixtures/certs/ec384-public.pem +0 -5
  41. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  42. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  43. data/spec/fixtures/certs/ec512-private.pem +0 -10
  44. data/spec/fixtures/certs/ec512-public.pem +0 -6
  45. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  46. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  47. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  48. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  49. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  50. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  51. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  52. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  53. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  54. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  55. data/spec/integration/readme_examples_spec.rb +0 -190
  56. data/spec/jwt/verify_spec.rb +0 -197
  57. data/spec/jwt_spec.rb +0 -240
  58. data/spec/spec_helper.rb +0 -31
@@ -0,0 +1,15 @@
1
+ module JWT
2
+ module DefaultOptions
3
+ DEFAULT_OPTIONS = {
4
+ verify_expiration: true,
5
+ verify_not_before: true,
6
+ verify_iss: false,
7
+ verify_iat: false,
8
+ verify_jti: false,
9
+ verify_aud: false,
10
+ verify_sub: false,
11
+ leeway: 0,
12
+ algorithms: ['HS256']
13
+ }.freeze
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './claims_validator'
4
+
5
+ # JWT::Encode module
6
+ module JWT
7
+ # Encoding logic for JWT
8
+ class Encode
9
+ ALG_NONE = 'none'.freeze
10
+ ALG_KEY = 'alg'.freeze
11
+
12
+ def initialize(options)
13
+ @payload = options[:payload]
14
+ @key = options[:key]
15
+ @algorithm = options[:algorithm]
16
+ @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
17
+ end
18
+
19
+ def segments
20
+ @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
+ end
22
+
23
+ private
24
+
25
+ def encoded_header
26
+ @encoded_header ||= encode_header
27
+ end
28
+
29
+ def encoded_payload
30
+ @encoded_payload ||= encode_payload
31
+ end
32
+
33
+ def encoded_signature
34
+ @encoded_signature ||= encode_signature
35
+ end
36
+
37
+ def encoded_header_and_payload
38
+ @encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
39
+ end
40
+
41
+ def encode_header
42
+ @headers[ALG_KEY] = @algorithm
43
+ encode(@headers)
44
+ end
45
+
46
+ def encode_payload
47
+ if @payload && @payload.is_a?(Hash)
48
+ ClaimsValidator.new(@payload).validate!
49
+ end
50
+
51
+ encode(@payload)
52
+ end
53
+
54
+ def encode_signature
55
+ return '' if @algorithm == ALG_NONE
56
+
57
+ JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
58
+ end
59
+
60
+ def encode(data)
61
+ JWT::Base64.url_encode(JWT::JSON.generate(data))
62
+ end
63
+
64
+ def combine(*parts)
65
+ parts.join('.')
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module JWT
4
+ class EncodeError < StandardError; end
3
5
  class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
+
4
8
  class VerificationError < DecodeError; end
5
9
  class ExpiredSignature < DecodeError; end
6
10
  class IncorrectAlgorithm < DecodeError; end
@@ -11,4 +15,6 @@ module JWT
11
15
  class InvalidSubError < DecodeError; end
12
16
  class InvalidJtiError < DecodeError; end
13
17
  class InvalidPayload < DecodeError; end
18
+
19
+ class JWKError < DecodeError; end
14
20
  end
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'json'
3
4
 
4
5
  module JWT
5
- # JSON fallback implementation or ruby 1.8.x
6
- module Json
7
- def decode_json(encoded)
8
- JSON.parse(encoded)
9
- rescue JSON::ParserError
10
- raise JWT::DecodeError, 'Invalid segment encoding'
11
- end
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
12
12
 
13
- def encode_json(raw)
14
- JSON.generate(raw)
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
15
16
  end
16
17
  end
17
18
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jwk/rsa'
4
+ require_relative 'jwk/key_finder'
5
+
6
+ module JWT
7
+ module JWK
8
+ MAPPINGS = {
9
+ 'RSA' => ::JWT::JWK::RSA,
10
+ OpenSSL::PKey::RSA => ::JWT::JWK::RSA
11
+ }.freeze
12
+
13
+ class << self
14
+ def import(jwk_data)
15
+ raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_data[:kty]
16
+
17
+ MAPPINGS.fetch(jwk_data[:kty].to_s) do |kty|
18
+ raise JWT::JWKError, "Key type #{kty} not supported"
19
+ end.import(jwk_data)
20
+ end
21
+
22
+ def create_from(keypair)
23
+ MAPPINGS.fetch(keypair.class) do |klass|
24
+ raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
25
+ end.new(keypair)
26
+ end
27
+
28
+ alias new create_from
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
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, "Could not find public key for kid #{kid}" unless jwk
18
+
19
+ ::JWT::JWK.import(jwk).keypair
20
+ end
21
+
22
+ private
23
+
24
+ def resolve_key(kid)
25
+ jwk = find_key(kid)
26
+
27
+ return jwk if jwk
28
+
29
+ if reloadable?
30
+ load_keys(invalidate: true)
31
+ return find_key(kid)
32
+ end
33
+
34
+ nil
35
+ end
36
+
37
+ def jwks
38
+ return @jwks if @jwks
39
+
40
+ load_keys
41
+ @jwks
42
+ end
43
+
44
+ def load_keys(opts = {})
45
+ @jwks = @jwk_loader.call(opts)
46
+ end
47
+
48
+ def find_key(kid)
49
+ Array(jwks[:keys]).find { |key| key[:kid] == kid }
50
+ end
51
+
52
+ def reloadable?
53
+ @jwk_loader
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class RSA
6
+ attr_reader :keypair
7
+
8
+ BINARY = 2
9
+ KTY = 'RSA'.freeze
10
+
11
+ def initialize(keypair)
12
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
+
14
+ @keypair = keypair
15
+ end
16
+
17
+ def private?
18
+ keypair.private?
19
+ end
20
+
21
+ def public_key
22
+ keypair.public_key
23
+ end
24
+
25
+ def kid
26
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
27
+ OpenSSL::ASN1::Integer.new(public_key.e)])
28
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
29
+ end
30
+
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
+ }
38
+ end
39
+
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)
46
+ 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)
49
+ end
50
+ self.new(imported_key)
51
+ end
52
+ end
53
+ end
54
+ 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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/security_utils'
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'
11
+ begin
12
+ require 'rbnacl'
13
+ rescue LoadError
14
+ raise if defined?(RbNaCl)
15
+ end
16
+
17
+ # JWT::Signature module
18
+ module JWT
19
+ # Signature logic for JWT
20
+ 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
30
+ ToSign = Struct.new(:algorithm, :msg, :key)
31
+ ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
32
+
33
+ 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)
38
+ end
39
+
40
+ 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
48
+ rescue OpenSSL::PKey::PKeyError
49
+ raise JWT::VerificationError, 'Signature verification raised'
50
+ ensure
51
+ OpenSSL.errors.clear
52
+ end
53
+ end
54
+ end
@@ -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