jwt 1.5.6 → 2.0.0
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/.ebert.yml +17 -0
- data/.reek.yml +40 -0
- data/.rubocop.yml +96 -0
- data/.travis.yml +9 -8
- data/CHANGELOG.md +82 -1
- data/Gemfile +0 -1
- data/README.md +71 -8
- data/lib/jwt/decode.rb +21 -29
- data/lib/jwt/default_options.rb +14 -0
- data/lib/jwt/encode.rb +51 -0
- data/lib/jwt/error.rb +2 -0
- data/lib/jwt/security_utils.rb +52 -0
- data/lib/jwt/signature.rb +106 -0
- data/lib/jwt/verify.rb +48 -53
- data/lib/jwt/version.rb +3 -3
- data/lib/jwt.rb +28 -159
- data/ruby-jwt.gemspec +4 -3
- data/spec/integration/readme_examples_spec.rb +20 -8
- data/spec/jwt/verify_spec.rb +64 -42
- data/spec/jwt_spec.rb +49 -32
- data/spec/spec_helper.rb +4 -7
- metadata +33 -15
- data/lib/jwt/json.rb +0 -17
@@ -0,0 +1,52 @@
|
|
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
|
+
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def secure_compare(left, right)
|
10
|
+
left_bytesize = left.bytesize
|
11
|
+
|
12
|
+
return false unless left_bytesize == right.bytesize
|
13
|
+
|
14
|
+
unpacked_left = left.unpack "C#{left_bytesize}"
|
15
|
+
result = 0
|
16
|
+
right.each_byte { |byte| result |= byte ^ unpacked_left.shift }
|
17
|
+
result.zero?
|
18
|
+
end
|
19
|
+
|
20
|
+
def verify_rsa(algorithm, public_key, signing_input, signature)
|
21
|
+
public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
|
22
|
+
end
|
23
|
+
|
24
|
+
def asn1_to_raw(signature, public_key)
|
25
|
+
byte_size = (public_key.group.degree + 7) / 8
|
26
|
+
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
|
27
|
+
end
|
28
|
+
|
29
|
+
def raw_to_asn1(signature, private_key)
|
30
|
+
byte_size = (private_key.group.degree + 7) / 8
|
31
|
+
sig_bytes = signature[0..(byte_size - 1)]
|
32
|
+
sig_char = signature[byte_size..-1] || ''
|
33
|
+
OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
34
|
+
end
|
35
|
+
|
36
|
+
def rbnacl_fixup(algorithm, key)
|
37
|
+
algorithm = algorithm.sub('HS', 'SHA').to_sym
|
38
|
+
|
39
|
+
return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
|
40
|
+
|
41
|
+
authenticator = RbNaCl::HMAC.const_get(algorithm)
|
42
|
+
|
43
|
+
# Fall back to OpenSSL for keys larger than 32 bytes.
|
44
|
+
return [] if key.bytesize > authenticator.key_bytes
|
45
|
+
|
46
|
+
[
|
47
|
+
authenticator,
|
48
|
+
key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
|
49
|
+
]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt/security_utils'
|
4
|
+
require 'openssl'
|
5
|
+
begin
|
6
|
+
require 'rbnacl'
|
7
|
+
rescue LoadError => e
|
8
|
+
abort(e.message) if defined?(RbNaCl)
|
9
|
+
end
|
10
|
+
|
11
|
+
# JWT::Signature module
|
12
|
+
module JWT
|
13
|
+
# Signature logic for JWT
|
14
|
+
module Signature
|
15
|
+
extend self
|
16
|
+
|
17
|
+
HMAC_ALGORITHMS = %w[HS256 HS512256 HS384 HS512].freeze
|
18
|
+
RSA_ALGORITHMS = %w[RS256 RS384 RS512].freeze
|
19
|
+
ECDSA_ALGORITHMS = %w[ES256 ES384 ES512].freeze
|
20
|
+
|
21
|
+
NAMED_CURVES = {
|
22
|
+
'prime256v1' => 'ES256',
|
23
|
+
'secp384r1' => 'ES384',
|
24
|
+
'secp521r1' => 'ES512'
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def sign(algorithm, msg, key)
|
28
|
+
if HMAC_ALGORITHMS.include?(algorithm)
|
29
|
+
sign_hmac(algorithm, msg, key)
|
30
|
+
elsif RSA_ALGORITHMS.include?(algorithm)
|
31
|
+
sign_rsa(algorithm, msg, key)
|
32
|
+
elsif ECDSA_ALGORITHMS.include?(algorithm)
|
33
|
+
sign_ecdsa(algorithm, msg, key)
|
34
|
+
else
|
35
|
+
raise NotImplementedError, 'Unsupported signing method'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def verify(algo, key, signing_input, signature)
|
40
|
+
verified = if HMAC_ALGORITHMS.include?(algo)
|
41
|
+
verify_hmac(algo, key, signing_input, signature)
|
42
|
+
elsif RSA_ALGORITHMS.include?(algo)
|
43
|
+
SecurityUtils.verify_rsa(algo, key, signing_input, signature)
|
44
|
+
elsif ECDSA_ALGORITHMS.include?(algo)
|
45
|
+
verify_ecdsa(algo, key, signing_input, signature)
|
46
|
+
else
|
47
|
+
raise JWT::VerificationError, 'Algorithm not supported'
|
48
|
+
end
|
49
|
+
|
50
|
+
raise(JWT::VerificationError, 'Signature verification raised') unless verified
|
51
|
+
rescue OpenSSL::PKey::PKeyError
|
52
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
53
|
+
ensure
|
54
|
+
OpenSSL.errors.clear
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def sign_rsa(algorithm, msg, private_key)
|
60
|
+
raise EncodeError, "The given key is a #{private_key.class}. It has to be an OpenSSL::PKey::RSA instance." if private_key.class == String
|
61
|
+
private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
|
62
|
+
end
|
63
|
+
|
64
|
+
def sign_ecdsa(algorithm, msg, private_key)
|
65
|
+
key_algorithm = NAMED_CURVES[private_key.group.curve_name]
|
66
|
+
if algorithm != key_algorithm
|
67
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
|
68
|
+
end
|
69
|
+
|
70
|
+
digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
|
71
|
+
SecurityUtils.asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
|
72
|
+
end
|
73
|
+
|
74
|
+
def sign_hmac(algorithm, msg, key)
|
75
|
+
authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
|
76
|
+
if authenticator && padded_key
|
77
|
+
authenticator.auth(padded_key, msg.encode('binary'))
|
78
|
+
else
|
79
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def verify_ecdsa(algorithm, public_key, signing_input, signature)
|
84
|
+
key_algorithm = NAMED_CURVES[public_key.group.curve_name]
|
85
|
+
if algorithm != key_algorithm
|
86
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
|
87
|
+
end
|
88
|
+
|
89
|
+
digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
|
90
|
+
public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
|
91
|
+
end
|
92
|
+
|
93
|
+
def verify_hmac(algorithm, public_key, signing_input, signature)
|
94
|
+
authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
|
95
|
+
if authenticator && padded_key
|
96
|
+
begin
|
97
|
+
authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
|
98
|
+
rescue RbNaCl::BadAuthenticatorError
|
99
|
+
false
|
100
|
+
end
|
101
|
+
else
|
102
|
+
SecurityUtils.secure_compare(signature, sign_hmac(algorithm, signing_input, public_key))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/lib/jwt/verify.rb
CHANGED
@@ -1,106 +1,101 @@
|
|
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
|
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 =
|
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
|
-
|
34
|
-
if options_aud.
|
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
|
-
|
55
|
-
|
56
|
-
end
|
47
|
+
iat = @payload['iat']
|
48
|
+
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > (Time.now.to_f + iat_leeway)
|
57
49
|
end
|
58
50
|
|
59
51
|
def verify_iss
|
60
|
-
return unless (options_iss =
|
52
|
+
return unless (options_iss = @options[:iss])
|
61
53
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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 =
|
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(
|
74
|
-
|
75
|
-
raise(JWT::InvalidJtiError, 'Missing jti')
|
66
|
+
raise(JWT::InvalidJtiError, 'Invalid jti') unless options_verify_jti.call(jti)
|
67
|
+
elsif jti.to_s.strip.empty?
|
68
|
+
raise(JWT::InvalidJtiError, 'Missing jti')
|
76
69
|
end
|
77
70
|
end
|
78
71
|
|
79
72
|
def verify_not_before
|
80
73
|
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
|
74
|
+
raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
|
85
75
|
end
|
86
76
|
|
87
77
|
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
|
78
|
+
return unless (options_sub = @options[:sub])
|
79
|
+
sub = @payload['sub']
|
80
|
+
raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
|
94
81
|
end
|
95
82
|
|
96
83
|
private
|
97
84
|
|
98
|
-
def
|
99
|
-
@options
|
85
|
+
def global_leeway
|
86
|
+
@options[:leeway]
|
87
|
+
end
|
88
|
+
|
89
|
+
def exp_leeway
|
90
|
+
@options[:exp_leeway] || global_leeway
|
91
|
+
end
|
92
|
+
|
93
|
+
def iat_leeway
|
94
|
+
@options[:iat_leeway] || global_leeway
|
100
95
|
end
|
101
96
|
|
102
|
-
def
|
103
|
-
|
97
|
+
def nbf_leeway
|
98
|
+
@options[:nbf_leeway] || global_leeway
|
104
99
|
end
|
105
100
|
end
|
106
101
|
end
|
data/lib/jwt/version.rb
CHANGED
data/lib/jwt.rb
CHANGED
@@ -1,192 +1,61 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'base64'
|
3
|
-
require 'openssl'
|
4
4
|
require 'jwt/decode'
|
5
|
+
require 'jwt/default_options'
|
6
|
+
require 'jwt/encode'
|
5
7
|
require 'jwt/error'
|
6
|
-
require 'jwt/
|
8
|
+
require 'jwt/signature'
|
9
|
+
require 'jwt/verify'
|
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
|
14
|
+
# https://tools.ietf.org/html/rfc7519
|
12
15
|
module JWT
|
13
|
-
|
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
|
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
|
21
|
+
encoder = Encode.new payload, key, algorithm, header_fields
|
22
|
+
encoder.segments
|
117
23
|
end
|
118
24
|
|
119
25
|
def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
|
120
26
|
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
|
121
27
|
|
122
28
|
merged_options = DEFAULT_OPTIONS.merge(custom_options)
|
123
|
-
|
29
|
+
|
30
|
+
decoder = Decode.new jwt, verify
|
124
31
|
header, payload, signature, signing_input = decoder.decode_segments
|
125
|
-
decode_verify_signature(key, header, signature, signing_input, merged_options, &keyfinder) if verify
|
126
|
-
|
32
|
+
decode_verify_signature(key, header, payload, signature, signing_input, merged_options, &keyfinder) if verify
|
33
|
+
|
34
|
+
Verify.verify_claims(payload, merged_options) if verify
|
127
35
|
|
128
36
|
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
129
37
|
|
130
38
|
[payload, header]
|
131
39
|
end
|
132
40
|
|
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
|
41
|
+
def decode_verify_signature(key, header, payload, signature, signing_input, options, &keyfinder)
|
42
|
+
algo, key = signature_algorithm_and_key(header, payload, key, &keyfinder)
|
140
43
|
|
141
|
-
|
142
|
-
|
143
|
-
[header['alg'], key]
|
144
|
-
end
|
44
|
+
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') unless options[:algorithm]
|
45
|
+
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless algo == options[:algorithm]
|
145
46
|
|
146
|
-
|
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
|
47
|
+
Signature.verify(algo, key, signing_input, signature)
|
152
48
|
end
|
153
49
|
|
154
|
-
def
|
155
|
-
if
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
raise JWT::VerificationError, 'Algorithm not supported'
|
50
|
+
def signature_algorithm_and_key(header, payload, key, &keyfinder)
|
51
|
+
if keyfinder
|
52
|
+
key = if keyfinder.arity == 2
|
53
|
+
yield(header, payload)
|
54
|
+
else
|
55
|
+
yield(header)
|
56
|
+
end
|
57
|
+
raise JWT::DecodeError, 'No verification key available' unless key
|
163
58
|
end
|
164
|
-
|
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
|
187
|
-
end
|
188
|
-
|
189
|
-
def base64url_decode(str)
|
190
|
-
Decode.base64url_decode(str)
|
59
|
+
[header['alg'], key]
|
191
60
|
end
|
192
61
|
end
|
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,17 +13,19 @@ 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) }
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
-
spec.require_paths = %w
|
21
|
+
spec.require_paths = %w[lib]
|
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
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require_relative '../spec_helper'
|
3
4
|
require 'jwt'
|
4
5
|
|
@@ -10,10 +11,10 @@ describe 'README.md code test' do
|
|
10
11
|
token = JWT.encode payload, nil, 'none'
|
11
12
|
decoded_token = JWT.decode token, nil, false
|
12
13
|
|
13
|
-
expect(token).to eq '
|
14
|
+
expect(token).to eq 'eyJhbGciOiJub25lIn0.eyJkYXRhIjoidGVzdCJ9.'
|
14
15
|
expect(decoded_token).to eq [
|
15
16
|
{ 'data' => 'test' },
|
16
|
-
{ '
|
17
|
+
{ 'alg' => 'none' }
|
17
18
|
]
|
18
19
|
end
|
19
20
|
|
@@ -21,10 +22,10 @@ describe 'README.md code test' do
|
|
21
22
|
token = JWT.encode payload, 'my$ecretK3y', 'HS256'
|
22
23
|
decoded_token = JWT.decode token, 'my$ecretK3y', false
|
23
24
|
|
24
|
-
expect(token).to eq '
|
25
|
+
expect(token).to eq 'eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pNIWIL34Jo13LViZAJACzK6Yf0qnvT_BuwOxiMCPE-Y'
|
25
26
|
expect(decoded_token).to eq [
|
26
27
|
{ 'data' => 'test' },
|
27
|
-
{ '
|
28
|
+
{ 'alg' => 'HS256' }
|
28
29
|
]
|
29
30
|
end
|
30
31
|
|
@@ -37,7 +38,7 @@ describe 'README.md code test' do
|
|
37
38
|
|
38
39
|
expect(decoded_token).to eq [
|
39
40
|
{ 'data' => 'test' },
|
40
|
-
{ '
|
41
|
+
{ 'alg' => 'RS256' }
|
41
42
|
]
|
42
43
|
end
|
43
44
|
|
@@ -52,7 +53,7 @@ describe 'README.md code test' do
|
|
52
53
|
|
53
54
|
expect(decoded_token).to eq [
|
54
55
|
{ 'data' => 'test' },
|
55
|
-
{ '
|
56
|
+
{ 'alg' => 'ES256' }
|
56
57
|
]
|
57
58
|
end
|
58
59
|
end
|
@@ -122,13 +123,13 @@ describe 'README.md code test' do
|
|
122
123
|
|
123
124
|
context 'aud' do
|
124
125
|
it 'array' do
|
125
|
-
aud = %w
|
126
|
+
aud = %w[Young Old]
|
126
127
|
aud_payload = { data: 'data', aud: aud }
|
127
128
|
|
128
129
|
token = JWT.encode aud_payload, hmac_secret, 'HS256'
|
129
130
|
|
130
131
|
expect do
|
131
|
-
JWT.decode token, hmac_secret, true, aud: %w
|
132
|
+
JWT.decode token, hmac_secret, true, aud: %w[Old Young], verify_aud: true, algorithm: 'HS256'
|
132
133
|
end.not_to raise_error
|
133
134
|
end
|
134
135
|
|
@@ -176,6 +177,17 @@ describe 'README.md code test' do
|
|
176
177
|
end
|
177
178
|
end
|
178
179
|
|
180
|
+
context 'custom header fields' do
|
181
|
+
it 'with custom field' do
|
182
|
+
payload = { data: 'test' }
|
183
|
+
|
184
|
+
token = JWT.encode payload, nil, 'none', typ: 'JWT'
|
185
|
+
_, header = JWT.decode token, nil, false
|
186
|
+
|
187
|
+
expect(header['typ']).to eq 'JWT'
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
179
191
|
it 'sub' do
|
180
192
|
sub = 'Subject'
|
181
193
|
sub_payload = { data: 'data', sub: sub }
|