jwt 1.5.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
- require 'jwt/json'
3
- require 'jwt/verify'
2
+ require 'json'
4
3
 
5
4
  # JWT::Decode module
6
5
  module JWT
7
- extend JWT::Json
8
-
9
6
  # Decoding logic for JWT
10
7
  class Decode
11
8
  attr_reader :header, :payload, :signature
12
9
 
13
- def initialize(jwt, key, verify, options, &keyfinder)
10
+ def self.base64url_decode(str)
11
+ str += '=' * (4 - str.length.modulo(4))
12
+ Base64.decode64(str.tr('-_', '+/'))
13
+ end
14
+
15
+ def initialize(jwt, verify)
14
16
  @jwt = jwt
15
- @key = key
16
17
  @verify = verify
17
- @options = options
18
- @keyfinder = keyfinder
19
18
  end
20
19
 
21
20
  def decode_segments
@@ -26,32 +25,21 @@ module JWT
26
25
  [@header, @payload, @signature, signing_input]
27
26
  end
28
27
 
28
+ private
29
+
29
30
  def raw_segments(jwt, verify)
30
31
  segments = jwt.split('.')
31
32
  required_num_segments = verify ? [3] : [2, 3]
32
33
  raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length
33
34
  segments
34
35
  end
35
- private :raw_segments
36
36
 
37
37
  def decode_header_and_payload(header_segment, payload_segment)
38
- header = JWT.decode_json(Decode.base64url_decode(header_segment))
39
- payload = JWT.decode_json(Decode.base64url_decode(payload_segment))
38
+ header = JSON.parse(Decode.base64url_decode(header_segment))
39
+ payload = JSON.parse(Decode.base64url_decode(payload_segment))
40
40
  [header, payload]
41
- end
42
- private :decode_header_and_payload
43
-
44
- def self.base64url_decode(str)
45
- str += '=' * (4 - str.length.modulo(4))
46
- Base64.decode64(str.tr('-_', '+/'))
47
- end
48
-
49
- def verify
50
- @options.each do |key, val|
51
- next unless key.to_s =~ /verify/
52
-
53
- Verify.send(key, payload, @options) if val
54
- end
41
+ rescue JSON::ParserError
42
+ raise JWT::DecodeError, 'Invalid segment encoding'
55
43
  end
56
44
  end
57
45
  end
@@ -0,0 +1,14 @@
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
+ }.freeze
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ # JWT::Encode module
5
+ module JWT
6
+ # Encoding logic for JWT
7
+ class Encode
8
+ attr_reader :payload, :key, :algorithm, :header_fields, :segments
9
+
10
+ def self.base64url_encode(str)
11
+ Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
12
+ end
13
+
14
+ def initialize(payload, key, algorithm, header_fields)
15
+ @payload = payload
16
+ @key = key
17
+ @algorithm = algorithm
18
+ @header_fields = header_fields
19
+ @segments = encode_segments
20
+ end
21
+
22
+ private
23
+
24
+ def encoded_header(algorithm, header_fields)
25
+ header = { 'alg' => algorithm }.merge(header_fields)
26
+ Encode.base64url_encode(JSON.generate(header))
27
+ end
28
+
29
+ def encoded_payload(payload)
30
+ raise InvalidPayload, 'exp claim must be an integer' if payload['exp'] && payload['exp'].is_a?(Time)
31
+ Encode.base64url_encode(JSON.generate(payload))
32
+ end
33
+
34
+ def encoded_signature(signing_input, key, algorithm)
35
+ if algorithm == 'none'
36
+ ''
37
+ else
38
+ signature = JWT::Signature.sign(algorithm, signing_input, key)
39
+ Encode.base64url_encode(signature)
40
+ end
41
+ end
42
+
43
+ def encode_segments
44
+ segments = []
45
+ segments << encoded_header(@algorithm, @header_fields)
46
+ segments << encoded_payload(@payload)
47
+ segments << encoded_signature(segments.join('.'), @key, @algorithm)
48
+ segments.join('.')
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module JWT
3
+ class EncodeError < StandardError; end
3
4
  class DecodeError < StandardError; end
4
5
  class VerificationError < DecodeError; end
5
6
  class ExpiredSignature < DecodeError; end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+ require 'openssl'
3
+ begin
4
+ require 'rbnacl'
5
+ rescue LoadError
6
+ end
7
+
8
+ # JWT::Signature module
9
+ module JWT
10
+ # Signature logic for JWT
11
+ module Signature
12
+ extend self
13
+
14
+ HMAC_ALGORITHMS = %w(HS256 HS512256 HS384 HS512).freeze
15
+ RSA_ALGORITHMS = %w(RS256 RS384 RS512).freeze
16
+ ECDSA_ALGORITHMS = %w(ES256 ES384 ES512).freeze
17
+
18
+ NAMED_CURVES = {
19
+ 'prime256v1' => 'ES256',
20
+ 'secp384r1' => 'ES384',
21
+ 'secp521r1' => 'ES512'
22
+ }.freeze
23
+
24
+ def sign(algorithm, msg, key)
25
+ if HMAC_ALGORITHMS.include?(algorithm)
26
+ sign_hmac(algorithm, msg, key)
27
+ elsif RSA_ALGORITHMS.include?(algorithm)
28
+ sign_rsa(algorithm, msg, key)
29
+ elsif ECDSA_ALGORITHMS.include?(algorithm)
30
+ sign_ecdsa(algorithm, msg, key)
31
+ else
32
+ raise NotImplementedError, 'Unsupported signing method'
33
+ end
34
+ end
35
+
36
+ def verify(algo, key, signing_input, signature)
37
+ verified = if HMAC_ALGORITHMS.include?(algo)
38
+ verify_hmac(algo, key, signing_input, signature)
39
+ elsif RSA_ALGORITHMS.include?(algo)
40
+ verify_rsa(algo, key, signing_input, signature)
41
+ elsif ECDSA_ALGORITHMS.include?(algo)
42
+ verify_ecdsa(algo, key, signing_input, signature)
43
+ else
44
+ raise JWT::VerificationError, 'Algorithm not supported'
45
+ end
46
+
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
+
54
+ private
55
+
56
+ def sign_rsa(algorithm, msg, private_key)
57
+ raise EncodeError, "The given key is a #{private_key.class}. It has to be an OpenSSL::PKey::RSA instance." if private_key.class == String
58
+ private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
59
+ end
60
+
61
+ def sign_ecdsa(algorithm, msg, private_key)
62
+ key_algorithm = NAMED_CURVES[private_key.group.curve_name]
63
+ if algorithm != key_algorithm
64
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
65
+ end
66
+
67
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
68
+ asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
69
+ end
70
+
71
+ def sign_hmac(algorithm, msg, key)
72
+ authenticator, padded_key = rbnacl_fixup(algorithm, key)
73
+ if authenticator && padded_key
74
+ authenticator.auth(padded_key, msg.encode('binary'))
75
+ else
76
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
77
+ end
78
+ end
79
+
80
+ def verify_rsa(algorithm, public_key, signing_input, signature)
81
+ public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
82
+ end
83
+
84
+ def verify_ecdsa(algorithm, public_key, signing_input, signature)
85
+ key_algorithm = NAMED_CURVES[public_key.group.curve_name]
86
+ if algorithm != key_algorithm
87
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
88
+ end
89
+
90
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
91
+ public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
92
+ end
93
+
94
+ def verify_hmac(algorithm, public_key, signing_input, signature)
95
+ authenticator, padded_key = rbnacl_fixup(algorithm, public_key)
96
+ if authenticator && padded_key
97
+ begin
98
+ authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
99
+ rescue RbNaCl::BadAuthenticatorError
100
+ false
101
+ end
102
+ else
103
+ secure_compare(signature, sign_hmac(algorithm, signing_input, public_key))
104
+ end
105
+ end
106
+
107
+ def asn1_to_raw(signature, public_key)
108
+ byte_size = (public_key.group.degree + 7) / 8
109
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
110
+ end
111
+
112
+ def raw_to_asn1(signature, private_key)
113
+ byte_size = (private_key.group.degree + 7) / 8
114
+ r = signature[0..(byte_size - 1)]
115
+ s = signature[byte_size..-1] || ''
116
+ OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
117
+ end
118
+
119
+ def rbnacl_fixup(algorithm, key)
120
+ algorithm = algorithm.sub('HS', 'SHA').to_sym
121
+
122
+ return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
123
+
124
+ authenticator = RbNaCl::HMAC.const_get(algorithm)
125
+
126
+ # Fall back to OpenSSL for keys larger than 32 bytes.
127
+ return [] if key.bytesize > authenticator.key_bytes
128
+
129
+ [
130
+ authenticator,
131
+ key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
132
+ ]
133
+ end
134
+
135
+ # From devise
136
+ # constant-time comparison algorithm to prevent timing attacks
137
+ def secure_compare(a, b)
138
+ return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
139
+ l = a.unpack "C#{a.bytesize}"
140
+ res = 0
141
+ b.each_byte { |byte| res |= byte ^ l.shift }
142
+ res.zero?
143
+ end
144
+ end
145
+ end
@@ -10,6 +10,13 @@ module JWT
10
10
  new(payload, options).send(method_name)
11
11
  end
12
12
  end
13
+
14
+ def verify_claims(payload, options)
15
+ options.each do |key, val|
16
+ next unless key.to_s =~ /verify/
17
+ Verify.send(key, payload, options) if val
18
+ end
19
+ end
13
20
  end
14
21
 
15
22
  def initialize(payload, options)
@@ -18,89 +25,60 @@ module JWT
18
25
  end
19
26
 
20
27
  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
32
-
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
28
+ return unless (options_aud = @options[:aud])
29
+ raise(JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{@payload['aud'] || '<none>'}") if ([*@payload['aud']] & [*options_aud]).empty?
41
30
  end
42
31
 
43
32
  def verify_expiration
44
33
  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
34
+ raise(JWT::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
49
35
  end
50
36
 
51
37
  def verify_iat
52
38
  return unless @payload.include?('iat')
53
-
54
- if !@payload['iat'].is_a?(Numeric) || @payload['iat'].to_f > (Time.now.to_f + leeway)
55
- raise(JWT::InvalidIatError, 'Invalid iat')
56
- end
39
+ raise(JWT::InvalidIatError, 'Invalid iat') if !@payload['iat'].is_a?(Numeric) || @payload['iat'].to_f > (Time.now.to_f + iat_leeway)
57
40
  end
58
41
 
59
42
  def verify_iss
60
- return unless (options_iss = extract_option(:iss))
61
-
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
43
+ return unless (options_iss = @options[:iss])
44
+ raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{@payload['iss'] || '<none>'}") if @payload['iss'].to_s != options_iss.to_s
68
45
  end
69
46
 
70
47
  def verify_jti
71
- options_verify_jti = extract_option(:verify_jti)
48
+ options_verify_jti = @options[:verify_jti]
72
49
  if options_verify_jti.respond_to?(:call)
73
50
  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?
51
+ elsif @payload['jti'].to_s.strip.empty?
52
+ raise(JWT::InvalidJtiError, 'Missing jti')
76
53
  end
77
54
  end
78
55
 
79
56
  def verify_not_before
80
57
  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
58
+ raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
85
59
  end
86
60
 
87
61
  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
62
+ return unless (options_sub = @options[:sub])
63
+ raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{@payload['sub'] || '<none>'}") unless @payload['sub'].to_s == options_sub.to_s
94
64
  end
95
65
 
96
66
  private
97
67
 
98
- def extract_option(key)
99
- @options.values_at(key.to_sym, key.to_s).compact.first
68
+ def global_leeway
69
+ @options[:leeway]
70
+ end
71
+
72
+ def exp_leeway
73
+ @options[:exp_leeway] || global_leeway
74
+ end
75
+
76
+ def iat_leeway
77
+ @options[:iat_leeway] || global_leeway
100
78
  end
101
79
 
102
- def leeway
103
- extract_option :leeway
80
+ def nbf_leeway
81
+ @options[:nbf_leeway] || global_leeway
104
82
  end
105
83
  end
106
84
  end
@@ -10,13 +10,13 @@ module JWT
10
10
  # Moments version builder module
11
11
  module VERSION
12
12
  # major version
13
- MAJOR = 1
13
+ MAJOR = 2
14
14
  # minor version
15
- MINOR = 5
15
+ MINOR = 0
16
16
  # tiny version
17
- TINY = 6
17
+ TINY = 0
18
18
  # alpha, beta, etc. tag
19
- PRE = nil
19
+ PRE = 'beta1'.freeze
20
20
 
21
21
  # Build version string
22
22
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -6,7 +6,6 @@ Gem::Specification.new do |spec|
6
6
  spec.name = 'jwt'
7
7
  spec.version = JWT.gem_version
8
8
  spec.authors = [
9
- 'Jeff Lindsay',
10
9
  'Tim Rudat'
11
10
  ]
12
11
  spec.email = 'timrudat@gmail.com'
@@ -14,6 +13,7 @@ Gem::Specification.new do |spec|
14
13
  spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.'
15
14
  spec.homepage = 'http://github.com/jwt/ruby-jwt'
16
15
  spec.license = 'MIT'
16
+ spec.required_ruby_version = '~> 2.1'
17
17
 
18
18
  spec.files = `git ls-files -z`.split("\x0")
19
19
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
@@ -22,9 +22,10 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency 'bundler'
24
24
  spec.add_development_dependency 'rake'
25
- spec.add_development_dependency 'json', '< 2.0'
26
25
  spec.add_development_dependency 'rspec'
27
26
  spec.add_development_dependency 'simplecov'
28
27
  spec.add_development_dependency 'simplecov-json'
29
28
  spec.add_development_dependency 'codeclimate-test-reporter'
29
+ spec.add_development_dependency 'codacy-coverage'
30
+ spec.add_development_dependency 'rbnacl'
30
31
  end