jwt 1.5.6 → 2.0.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.
@@ -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