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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +8 -8
- data/CHANGELOG.md +48 -0
- data/README.md +63 -7
- data/lib/jwt.rb +31 -156
- data/lib/jwt/decode.rb +13 -25
- data/lib/jwt/default_options.rb +14 -0
- data/lib/jwt/encode.rb +51 -0
- data/lib/jwt/error.rb +1 -0
- data/lib/jwt/signature.rb +145 -0
- data/lib/jwt/verify.rb +31 -53
- data/lib/jwt/version.rb +4 -4
- data/ruby-jwt.gemspec +3 -2
- data/spec/integration/readme_examples_spec.rb +17 -6
- data/spec/jwt/verify_spec.rb +19 -26
- data/spec/jwt_spec.rb +27 -34
- data/spec/spec_helper.rb +3 -6
- metadata +33 -18
- data/lib/jwt/json.rb +0 -17
data/lib/jwt/decode.rb
CHANGED
@@ -1,21 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require '
|
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
|
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 =
|
39
|
-
payload =
|
38
|
+
header = JSON.parse(Decode.base64url_decode(header_segment))
|
39
|
+
payload = JSON.parse(Decode.base64url_decode(payload_segment))
|
40
40
|
[header, payload]
|
41
|
-
|
42
|
-
|
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
|
data/lib/jwt/encode.rb
ADDED
@@ -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
|
data/lib/jwt/error.rb
CHANGED
@@ -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
|
data/lib/jwt/verify.rb
CHANGED
@@ -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 =
|
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 =
|
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 =
|
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
|
-
|
75
|
-
raise(JWT::InvalidJtiError, 'Missing jti')
|
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 =
|
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
|
99
|
-
@options
|
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
|
103
|
-
|
80
|
+
def nbf_leeway
|
81
|
+
@options[:nbf_leeway] || global_leeway
|
104
82
|
end
|
105
83
|
end
|
106
84
|
end
|
data/lib/jwt/version.rb
CHANGED
@@ -10,13 +10,13 @@ module JWT
|
|
10
10
|
# Moments version builder module
|
11
11
|
module VERSION
|
12
12
|
# major version
|
13
|
-
MAJOR =
|
13
|
+
MAJOR = 2
|
14
14
|
# minor version
|
15
|
-
MINOR =
|
15
|
+
MINOR = 0
|
16
16
|
# tiny version
|
17
|
-
TINY =
|
17
|
+
TINY = 0
|
18
18
|
# alpha, beta, etc. tag
|
19
|
-
PRE =
|
19
|
+
PRE = 'beta1'.freeze
|
20
20
|
|
21
21
|
# Build version string
|
22
22
|
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
data/ruby-jwt.gemspec
CHANGED
@@ -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
|