jwt 1.5.1 → 2.1.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/.codeclimate.yml +20 -0
- data/.ebert.yml +18 -0
- data/.gitignore +11 -0
- data/.reek.yml +40 -0
- data/.rspec +1 -0
- data/.rubocop.yml +98 -0
- data/.travis.yml +14 -0
- data/CHANGELOG.md +476 -0
- data/Gemfile +3 -0
- data/LICENSE +7 -0
- data/Manifest +3 -1
- data/README.md +478 -0
- data/Rakefile +8 -15
- data/lib/jwt/algos/ecdsa.rb +35 -0
- data/lib/jwt/algos/eddsa.rb +23 -0
- data/lib/jwt/algos/hmac.rb +33 -0
- data/lib/jwt/algos/rsa.rb +19 -0
- data/lib/jwt/algos/unsupported.rb +16 -0
- data/lib/jwt/decode.rb +49 -0
- data/lib/jwt/default_options.rb +15 -0
- data/lib/jwt/encode.rb +51 -0
- data/lib/jwt/error.rb +16 -0
- data/lib/jwt/security_utils.rb +51 -0
- data/lib/jwt/signature.rb +50 -0
- data/lib/jwt/verify.rb +102 -0
- data/lib/jwt/version.rb +24 -0
- data/lib/jwt.rb +33 -203
- data/ruby-jwt.gemspec +31 -0
- data/spec/fixtures/certs/ec256-private.pem +8 -0
- data/spec/fixtures/certs/ec256-public.pem +4 -0
- data/spec/fixtures/certs/ec256-wrong-private.pem +8 -0
- data/spec/fixtures/certs/ec256-wrong-public.pem +4 -0
- data/spec/fixtures/certs/ec384-private.pem +9 -0
- data/spec/fixtures/certs/ec384-public.pem +5 -0
- data/spec/fixtures/certs/ec384-wrong-private.pem +9 -0
- data/spec/fixtures/certs/ec384-wrong-public.pem +5 -0
- data/spec/fixtures/certs/ec512-private.pem +10 -0
- data/spec/fixtures/certs/ec512-public.pem +6 -0
- data/spec/fixtures/certs/ec512-wrong-private.pem +10 -0
- data/spec/fixtures/certs/ec512-wrong-public.pem +6 -0
- data/spec/fixtures/certs/rsa-1024-private.pem +15 -0
- data/spec/fixtures/certs/rsa-1024-public.pem +6 -0
- data/spec/fixtures/certs/rsa-2048-private.pem +27 -0
- data/spec/fixtures/certs/rsa-2048-public.pem +9 -0
- data/spec/fixtures/certs/rsa-2048-wrong-private.pem +27 -0
- data/spec/fixtures/certs/rsa-2048-wrong-public.pem +9 -0
- data/spec/fixtures/certs/rsa-4096-private.pem +51 -0
- data/spec/fixtures/certs/rsa-4096-public.pem +14 -0
- data/spec/integration/readme_examples_spec.rb +202 -0
- data/spec/jwt/verify_spec.rb +232 -0
- data/spec/jwt_spec.rb +236 -384
- data/spec/spec_helper.rb +28 -0
- metadata +187 -26
- data/jwt.gemspec +0 -34
- data/lib/jwt/json.rb +0 -32
- data/spec/helper.rb +0 -19
@@ -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
|
data/lib/jwt/encode.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
# JWT::Encode module
|
6
|
+
module JWT
|
7
|
+
# Encoding logic for JWT
|
8
|
+
class Encode
|
9
|
+
attr_reader :payload, :key, :algorithm, :header_fields, :segments
|
10
|
+
|
11
|
+
def self.base64url_encode(str)
|
12
|
+
Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(payload, key, algorithm, header_fields)
|
16
|
+
@payload = payload
|
17
|
+
@key = key
|
18
|
+
@algorithm = algorithm
|
19
|
+
@header_fields = header_fields
|
20
|
+
@segments = encode_segments
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def encoded_header
|
26
|
+
header = { 'alg' => @algorithm }.merge(@header_fields)
|
27
|
+
Encode.base64url_encode(JSON.generate(header))
|
28
|
+
end
|
29
|
+
|
30
|
+
def encoded_payload
|
31
|
+
raise InvalidPayload, 'exp claim must be an integer' if @payload && @payload.is_a?(Hash) && @payload.key?('exp') && !@payload['exp'].is_a?(Integer)
|
32
|
+
Encode.base64url_encode(JSON.generate(@payload))
|
33
|
+
end
|
34
|
+
|
35
|
+
def encoded_signature(signing_input)
|
36
|
+
if @algorithm == 'none'
|
37
|
+
''
|
38
|
+
else
|
39
|
+
signature = JWT::Signature.sign(@algorithm, signing_input, @key)
|
40
|
+
Encode.base64url_encode(signature)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def encode_segments
|
45
|
+
header = encoded_header
|
46
|
+
payload = encoded_payload
|
47
|
+
signature = encoded_signature([header, payload].join('.'))
|
48
|
+
[header, payload, signature].join('.')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/jwt/error.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
class EncodeError < StandardError; end
|
5
|
+
class DecodeError < StandardError; end
|
6
|
+
class VerificationError < DecodeError; end
|
7
|
+
class ExpiredSignature < DecodeError; end
|
8
|
+
class IncorrectAlgorithm < DecodeError; end
|
9
|
+
class ImmatureSignature < DecodeError; end
|
10
|
+
class InvalidIssuerError < DecodeError; end
|
11
|
+
class InvalidIatError < DecodeError; end
|
12
|
+
class InvalidAudError < DecodeError; end
|
13
|
+
class InvalidSubError < DecodeError; end
|
14
|
+
class InvalidJtiError < DecodeError; end
|
15
|
+
class InvalidPayload < DecodeError; end
|
16
|
+
end
|
@@ -0,0 +1,51 @@
|
|
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 asn1_to_raw(signature, public_key)
|
24
|
+
byte_size = (public_key.group.degree + 7) / 8
|
25
|
+
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
|
26
|
+
end
|
27
|
+
|
28
|
+
def raw_to_asn1(signature, private_key)
|
29
|
+
byte_size = (private_key.group.degree + 7) / 8
|
30
|
+
sig_bytes = signature[0..(byte_size - 1)]
|
31
|
+
sig_char = signature[byte_size..-1] || ''
|
32
|
+
OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
33
|
+
end
|
34
|
+
|
35
|
+
def rbnacl_fixup(algorithm, key)
|
36
|
+
algorithm = algorithm.sub('HS', 'SHA').to_sym
|
37
|
+
|
38
|
+
return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
|
39
|
+
|
40
|
+
authenticator = RbNaCl::HMAC.const_get(algorithm)
|
41
|
+
|
42
|
+
# Fall back to OpenSSL for keys larger than 32 bytes.
|
43
|
+
return [] if key.bytesize > authenticator.key_bytes
|
44
|
+
|
45
|
+
[
|
46
|
+
authenticator,
|
47
|
+
key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
|
48
|
+
]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,50 @@
|
|
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/unsupported'
|
10
|
+
begin
|
11
|
+
require 'rbnacl'
|
12
|
+
rescue LoadError
|
13
|
+
raise if defined?(RbNaCl)
|
14
|
+
end
|
15
|
+
|
16
|
+
# JWT::Signature module
|
17
|
+
module JWT
|
18
|
+
# Signature logic for JWT
|
19
|
+
module Signature
|
20
|
+
extend self
|
21
|
+
ALGOS = [
|
22
|
+
Algos::Hmac,
|
23
|
+
Algos::Ecdsa,
|
24
|
+
Algos::Rsa,
|
25
|
+
Algos::Eddsa,
|
26
|
+
Algos::Unsupported
|
27
|
+
].freeze
|
28
|
+
ToSign = Struct.new(:algorithm, :msg, :key)
|
29
|
+
ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
|
30
|
+
|
31
|
+
def sign(algorithm, msg, key)
|
32
|
+
algo = ALGOS.find do |alg|
|
33
|
+
alg.const_get(:SUPPORTED).include? algorithm
|
34
|
+
end
|
35
|
+
algo.sign ToSign.new(algorithm, msg, key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def verify(algorithm, key, signing_input, signature)
|
39
|
+
algo = ALGOS.find do |alg|
|
40
|
+
alg.const_get(:SUPPORTED).include? algorithm
|
41
|
+
end
|
42
|
+
verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature))
|
43
|
+
raise(JWT::VerificationError, 'Signature verification raised') unless verified
|
44
|
+
rescue OpenSSL::PKey::PKeyError
|
45
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
46
|
+
ensure
|
47
|
+
OpenSSL.errors.clear
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/jwt/verify.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt/error'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
# JWT verify methods
|
7
|
+
class Verify
|
8
|
+
DEFAULTS = {
|
9
|
+
leeway: 0
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
%w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method_name|
|
14
|
+
define_method method_name do |payload, options|
|
15
|
+
new(payload, options).send(method_name)
|
16
|
+
end
|
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
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(payload, options)
|
28
|
+
@payload = payload
|
29
|
+
@options = DEFAULTS.merge(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def verify_aud
|
33
|
+
return unless (options_aud = @options[:aud])
|
34
|
+
|
35
|
+
aud = @payload['aud']
|
36
|
+
raise(JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{aud || '<none>'}") if ([*aud] & [*options_aud]).empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def verify_expiration
|
40
|
+
return unless @payload.include?('exp')
|
41
|
+
raise(JWT::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
|
42
|
+
end
|
43
|
+
|
44
|
+
def verify_iat
|
45
|
+
return unless @payload.include?('iat')
|
46
|
+
|
47
|
+
iat = @payload['iat']
|
48
|
+
raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > (Time.now.to_f + iat_leeway)
|
49
|
+
end
|
50
|
+
|
51
|
+
def verify_iss
|
52
|
+
return unless (options_iss = @options[:iss])
|
53
|
+
|
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>'}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def verify_jti
|
62
|
+
options_verify_jti = @options[:verify_jti]
|
63
|
+
jti = @payload['jti']
|
64
|
+
|
65
|
+
if options_verify_jti.respond_to?(:call)
|
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')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def verify_not_before
|
74
|
+
return unless @payload.include?('nbf')
|
75
|
+
raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
|
76
|
+
end
|
77
|
+
|
78
|
+
def verify_sub
|
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
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def global_leeway
|
87
|
+
@options[:leeway]
|
88
|
+
end
|
89
|
+
|
90
|
+
def exp_leeway
|
91
|
+
@options[:exp_leeway] || global_leeway
|
92
|
+
end
|
93
|
+
|
94
|
+
def iat_leeway
|
95
|
+
@options[:iat_leeway] || global_leeway
|
96
|
+
end
|
97
|
+
|
98
|
+
def nbf_leeway
|
99
|
+
@options[:nbf_leeway] || global_leeway
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/jwt/version.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Moments version builder module
|
5
|
+
module JWT
|
6
|
+
def self.gem_version
|
7
|
+
Gem::Version.new VERSION::STRING
|
8
|
+
end
|
9
|
+
|
10
|
+
# Moments version builder module
|
11
|
+
module VERSION
|
12
|
+
# major version
|
13
|
+
MAJOR = 2
|
14
|
+
# minor version
|
15
|
+
MINOR = 1
|
16
|
+
# tiny version
|
17
|
+
TINY = 0
|
18
|
+
# alpha, beta, etc. tag
|
19
|
+
PRE = nil
|
20
|
+
|
21
|
+
# Build version string
|
22
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
23
|
+
end
|
24
|
+
end
|
data/lib/jwt.rb
CHANGED
@@ -1,233 +1,63 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'base64'
|
4
|
-
require '
|
5
|
-
require 'jwt/
|
4
|
+
require 'jwt/decode'
|
5
|
+
require 'jwt/default_options'
|
6
|
+
require 'jwt/encode'
|
7
|
+
require 'jwt/error'
|
8
|
+
require 'jwt/signature'
|
9
|
+
require 'jwt/verify'
|
6
10
|
|
7
11
|
# JSON Web Token implementation
|
8
12
|
#
|
9
13
|
# Should be up to date with the latest spec:
|
10
|
-
#
|
14
|
+
# https://tools.ietf.org/html/rfc7519
|
11
15
|
module JWT
|
12
|
-
|
13
|
-
class VerificationError < DecodeError; end
|
14
|
-
class ExpiredSignature < DecodeError; end
|
15
|
-
class IncorrectAlgorithm < DecodeError; end
|
16
|
-
class ImmatureSignature < DecodeError; end
|
17
|
-
class InvalidIssuerError < DecodeError; end
|
18
|
-
class InvalidIatError < DecodeError; end
|
19
|
-
class InvalidAudError < DecodeError; end
|
20
|
-
class InvalidSubError < DecodeError; end
|
21
|
-
class InvalidJtiError < DecodeError; end
|
22
|
-
extend JWT::Json
|
23
|
-
|
24
|
-
NAMED_CURVES = {
|
25
|
-
'prime256v1' => 'ES256',
|
26
|
-
'secp384r1' => 'ES384',
|
27
|
-
'secp521r1' => 'ES512'
|
28
|
-
}
|
16
|
+
include JWT::DefaultOptions
|
29
17
|
|
30
18
|
module_function
|
31
19
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
elsif ['RS256', 'RS384', 'RS512'].include?(algorithm)
|
36
|
-
sign_rsa(algorithm, msg, key)
|
37
|
-
elsif ['ES256', 'ES384', 'ES512'].include?(algorithm)
|
38
|
-
sign_ecdsa(algorithm, msg, key)
|
39
|
-
else
|
40
|
-
fail NotImplementedError.new('Unsupported signing method')
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def sign_rsa(algorithm, msg, private_key)
|
45
|
-
private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
|
46
|
-
end
|
47
|
-
|
48
|
-
def sign_ecdsa(algorithm, msg, private_key)
|
49
|
-
key_algorithm = NAMED_CURVES[private_key.group.curve_name]
|
50
|
-
if algorithm != key_algorithm
|
51
|
-
fail IncorrectAlgorithm.new("payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided")
|
52
|
-
end
|
53
|
-
|
54
|
-
digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
|
55
|
-
asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
|
56
|
-
end
|
57
|
-
|
58
|
-
def verify_rsa(algorithm, public_key, signing_input, signature)
|
59
|
-
public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
|
60
|
-
end
|
61
|
-
|
62
|
-
def verify_ecdsa(algorithm, public_key, signing_input, signature)
|
63
|
-
key_algorithm = NAMED_CURVES[public_key.group.curve_name]
|
64
|
-
if algorithm != key_algorithm
|
65
|
-
fail IncorrectAlgorithm.new("payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided")
|
66
|
-
end
|
67
|
-
|
68
|
-
digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
|
69
|
-
public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
|
20
|
+
def encode(payload, key, algorithm = 'HS256', header_fields = {})
|
21
|
+
encoder = Encode.new payload, key, algorithm, header_fields
|
22
|
+
encoder.segments
|
70
23
|
end
|
71
24
|
|
72
|
-
def
|
73
|
-
|
74
|
-
end
|
25
|
+
def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
|
26
|
+
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
|
75
27
|
|
76
|
-
|
77
|
-
str += '=' * (4 - str.length.modulo(4))
|
78
|
-
Base64.decode64(str.tr('-_', '+/'))
|
79
|
-
end
|
28
|
+
merged_options = DEFAULT_OPTIONS.merge(custom_options)
|
80
29
|
|
81
|
-
|
82
|
-
|
83
|
-
|
30
|
+
decoder = Decode.new jwt, verify
|
31
|
+
header, payload, signature, signing_input = decoder.decode_segments
|
32
|
+
decode_verify_signature(key, header, payload, signature, signing_input, merged_options, &keyfinder) if verify
|
84
33
|
|
85
|
-
|
86
|
-
header = { 'typ' => 'JWT', 'alg' => algorithm }.merge(header_fields)
|
87
|
-
base64url_encode(encode_json(header))
|
88
|
-
end
|
34
|
+
Verify.verify_claims(payload, merged_options) if verify
|
89
35
|
|
90
|
-
|
91
|
-
base64url_encode(encode_json(payload))
|
92
|
-
end
|
93
|
-
|
94
|
-
def encoded_signature(signing_input, key, algorithm)
|
95
|
-
if algorithm == 'none'
|
96
|
-
''
|
97
|
-
else
|
98
|
-
signature = sign(algorithm, signing_input, key)
|
99
|
-
base64url_encode(signature)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
def encode(payload, key, algorithm='HS256', header_fields={})
|
104
|
-
algorithm ||= 'none'
|
105
|
-
segments = []
|
106
|
-
segments << encoded_header(algorithm, header_fields)
|
107
|
-
segments << encoded_payload(payload)
|
108
|
-
segments << encoded_signature(segments.join('.'), key, algorithm)
|
109
|
-
segments.join('.')
|
110
|
-
end
|
36
|
+
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
111
37
|
|
112
|
-
|
113
|
-
segments = jwt.split('.')
|
114
|
-
required_num_segments = verify ? [3] : [2, 3]
|
115
|
-
fail JWT::DecodeError.new('Not enough or too many segments') unless required_num_segments.include? segments.length
|
116
|
-
segments
|
117
|
-
end
|
118
|
-
|
119
|
-
def decode_header_and_payload(header_segment, payload_segment)
|
120
|
-
header = decode_json(base64url_decode(header_segment))
|
121
|
-
payload = decode_json(base64url_decode(payload_segment))
|
122
|
-
[header, payload]
|
123
|
-
end
|
124
|
-
|
125
|
-
def decoded_segments(jwt, verify=true)
|
126
|
-
header_segment, payload_segment, crypto_segment = raw_segments(jwt, verify)
|
127
|
-
header, payload = decode_header_and_payload(header_segment, payload_segment)
|
128
|
-
signature = base64url_decode(crypto_segment.to_s) if verify
|
129
|
-
signing_input = [header_segment, payload_segment].join('.')
|
130
|
-
[header, payload, signature, signing_input]
|
38
|
+
[payload, header]
|
131
39
|
end
|
132
40
|
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
header, payload, signature, signing_input = decoded_segments(jwt, verify)
|
137
|
-
fail JWT::DecodeError.new('Not enough or too many segments') unless header && payload
|
138
|
-
|
139
|
-
default_options = {
|
140
|
-
:verify_expiration => true,
|
141
|
-
:verify_not_before => true,
|
142
|
-
:verify_iss => false,
|
143
|
-
:verify_iat => false,
|
144
|
-
:verify_jti => false,
|
145
|
-
:verify_aud => false,
|
146
|
-
:verify_sub => false,
|
147
|
-
:leeway => 0
|
148
|
-
}
|
41
|
+
def decode_verify_signature(key, header, payload, signature, signing_input, options, &keyfinder)
|
42
|
+
algo, key = signature_algorithm_and_key(header, payload, key, &keyfinder)
|
149
43
|
|
150
|
-
|
44
|
+
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms(options).empty?
|
45
|
+
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless allowed_algorithms(options).include?(algo)
|
151
46
|
|
152
|
-
|
153
|
-
algo, key = signature_algorithm_and_key(header, key, &keyfinder)
|
154
|
-
if options[:algorithm] && algo != options[:algorithm]
|
155
|
-
fail JWT::IncorrectAlgorithm.new('Expected a different algorithm')
|
156
|
-
end
|
157
|
-
verify_signature(algo, key, signing_input, signature)
|
158
|
-
end
|
159
|
-
|
160
|
-
if options[:verify_expiration] && payload.include?('exp')
|
161
|
-
fail JWT::ExpiredSignature.new('Signature has expired') unless payload['exp'].to_i > (Time.now.to_i - options[:leeway])
|
162
|
-
end
|
163
|
-
if options[:verify_not_before] && payload.include?('nbf')
|
164
|
-
fail JWT::ImmatureSignature.new('Signature nbf has not been reached') unless payload['nbf'].to_i < (Time.now.to_i + options[:leeway])
|
165
|
-
end
|
166
|
-
if options[:verify_iss] && options['iss']
|
167
|
-
fail JWT::InvalidIssuerError.new("Invalid issuer. Expected #{options['iss']}, received #{payload['iss'] || '<none>'}") unless payload['iss'].to_s == options['iss'].to_s
|
168
|
-
end
|
169
|
-
if options[:verify_iat] && payload.include?('iat')
|
170
|
-
fail JWT::InvalidIatError.new('Invalid iat') unless payload['iat'].is_a?(Integer) && payload['iat'].to_i <= Time.now.to_i
|
171
|
-
end
|
172
|
-
if options[:verify_aud] && options['aud']
|
173
|
-
if payload['aud'].is_a?(Array)
|
174
|
-
fail JWT::InvalidAudError.new('Invalid audience') unless payload['aud'].include?(options['aud'].to_s)
|
175
|
-
else
|
176
|
-
fail JWT::InvalidAudError.new("Invalid audience. Expected #{options['aud']}, received #{payload['aud'] || '<none>'}") unless payload['aud'].to_s == options['aud'].to_s
|
177
|
-
end
|
178
|
-
end
|
179
|
-
if options[:verify_sub] && payload.include?('sub')
|
180
|
-
fail JWT::InvalidSubError.new("Invalid subject. Expected #{options['sub']}, received #{payload['sub']}") unless payload['sub'].to_s == options['sub'].to_s
|
181
|
-
end
|
182
|
-
if options[:verify_jti] && payload.include?('jti')
|
183
|
-
fail JWT::InvalidJtiError.new('need iat for verify jwt id') unless payload.include?('iat')
|
184
|
-
fail JWT::InvalidJtiError.new('Not a uniq jwt id') unless options['jti'].to_s == Digest::MD5.hexdigest("#{key}:#{payload['iat']}")
|
185
|
-
end
|
186
|
-
|
187
|
-
[payload, header]
|
47
|
+
Signature.verify(algo, key, signing_input, signature)
|
188
48
|
end
|
189
49
|
|
190
|
-
def signature_algorithm_and_key(header, key, &keyfinder)
|
191
|
-
key = keyfinder.
|
50
|
+
def signature_algorithm_and_key(header, payload, key, &keyfinder)
|
51
|
+
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header)) if keyfinder
|
52
|
+
raise JWT::DecodeError, 'No verification key available' unless key
|
192
53
|
[header['alg'], key]
|
193
54
|
end
|
194
55
|
|
195
|
-
def
|
196
|
-
if
|
197
|
-
|
198
|
-
elsif ['RS256', 'RS384', 'RS512'].include?(algo)
|
199
|
-
fail JWT::VerificationError.new('Signature verification failed') unless verify_rsa(algo, key, signing_input, signature)
|
200
|
-
elsif ['ES256', 'ES384', 'ES512'].include?(algo)
|
201
|
-
fail JWT::VerificationError.new('Signature verification failed') unless verify_ecdsa(algo, key, signing_input, signature)
|
56
|
+
def allowed_algorithms(options)
|
57
|
+
if options.key?(:algorithm)
|
58
|
+
[options[:algorithm]]
|
202
59
|
else
|
203
|
-
|
60
|
+
options[:algorithms] || []
|
204
61
|
end
|
205
|
-
rescue OpenSSL::PKey::PKeyError
|
206
|
-
raise JWT::VerificationError.new('Signature verification failed')
|
207
|
-
ensure
|
208
|
-
OpenSSL.errors.clear
|
209
|
-
end
|
210
|
-
|
211
|
-
# From devise
|
212
|
-
# constant-time comparison algorithm to prevent timing attacks
|
213
|
-
def secure_compare(a, b)
|
214
|
-
return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
|
215
|
-
l = a.unpack "C#{a.bytesize}"
|
216
|
-
|
217
|
-
res = 0
|
218
|
-
b.each_byte { |byte| res |= byte ^ l.shift }
|
219
|
-
res == 0
|
220
|
-
end
|
221
|
-
|
222
|
-
def raw_to_asn1(signature, private_key)
|
223
|
-
byte_size = (private_key.group.degree + 7) / 8
|
224
|
-
r = signature[0..(byte_size - 1)]
|
225
|
-
s = signature[byte_size..-1]
|
226
|
-
OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
227
|
-
end
|
228
|
-
|
229
|
-
def asn1_to_raw(signature, public_key)
|
230
|
-
byte_size = (public_key.group.degree + 7) / 8
|
231
|
-
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
|
232
62
|
end
|
233
63
|
end
|
data/ruby-jwt.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
lib = File.expand_path('../lib/', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'jwt/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'jwt'
|
7
|
+
spec.version = JWT.gem_version
|
8
|
+
spec.authors = [
|
9
|
+
'Tim Rudat'
|
10
|
+
]
|
11
|
+
spec.email = 'timrudat@gmail.com'
|
12
|
+
spec.summary = 'JSON Web Token implementation in Ruby'
|
13
|
+
spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.'
|
14
|
+
spec.homepage = 'http://github.com/jwt/ruby-jwt'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.required_ruby_version = '>= 2.1'
|
17
|
+
|
18
|
+
spec.files = `git ls-files -z`.split("\x0")
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = %w[lib]
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'simplecov'
|
27
|
+
spec.add_development_dependency 'simplecov-json'
|
28
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
29
|
+
spec.add_development_dependency 'codacy-coverage'
|
30
|
+
spec.add_development_dependency 'rbnacl'
|
31
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
-----BEGIN EC PARAMETERS-----
|
2
|
+
BggqhkjOPQMBBw==
|
3
|
+
-----END EC PARAMETERS-----
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
5
|
+
MHcCAQEEIJmVse5uPfj6B4TcXrUAvf9/8pJh+KrKKYLNcmOnp/vPoAoGCCqGSM49
|
6
|
+
AwEHoUQDQgAEAr+WbDE5VtIDGhtYMxvEc6cMsDBc/DX1wuhIMu8dQzOLSt0tpqK9
|
7
|
+
MVfXbVfrKdayVFgoWzs8MilcYq0QIhKx/w==
|
8
|
+
-----END EC PRIVATE KEY-----
|
@@ -0,0 +1,8 @@
|
|
1
|
+
-----BEGIN EC PARAMETERS-----
|
2
|
+
BgUrgQQACg==
|
3
|
+
-----END EC PARAMETERS-----
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
5
|
+
MHQCAQEEICfA4AaomONdmPTzeyrx5U/jugYXTERyb5U3ETTv7Hx7oAcGBSuBBAAK
|
6
|
+
oUQDQgAEPmuXZT3jpJnEMVPOW6RMsmxeGLOCE1PN6fwvUwOsxv7YnyoQ5/bpo64n
|
7
|
+
+Jp4slSl1aUNoCBF2oz9bS0iyBo3jg==
|
8
|
+
-----END EC PRIVATE KEY-----
|
@@ -0,0 +1,9 @@
|
|
1
|
+
-----BEGIN EC PARAMETERS-----
|
2
|
+
BgUrgQQAIg==
|
3
|
+
-----END EC PARAMETERS-----
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
5
|
+
MIGkAgEBBDDxOljqUKw9YNhkluSJIBAYO1YXcNtS+vckd5hpTZ5toxsOlwbmyrnU
|
6
|
+
Tn+D5Xma1m2gBwYFK4EEACKhZANiAASQwYTiRvXu1hMHceSosMs/8uf50sJI3jvK
|
7
|
+
kdSkvuRAPxSzhtrUvCQDnVsThFq4aOdZZY1qh2ErJGtzmrx+pEsJvJnvfOTG3NGU
|
8
|
+
KRalek+LQfVqAUSvDMKlxdkz2e67tso=
|
9
|
+
-----END EC PRIVATE KEY-----
|
@@ -0,0 +1,9 @@
|
|
1
|
+
-----BEGIN EC PARAMETERS-----
|
2
|
+
BgUrgQQAIg==
|
3
|
+
-----END EC PARAMETERS-----
|
4
|
+
-----BEGIN EC PRIVATE KEY-----
|
5
|
+
MIGkAgEBBDAfZW47dSKnC5JkSVOk1ERxCIi/IJ1p1WBnVGx4hnrNHy+dxtaZJaF+
|
6
|
+
YLInFQ/QbYegBwYFK4EEACKhZANiAAQwXkx4BFBGLXbzl5yVrfxK7er8hSi38iDE
|
7
|
+
K2+7cdrR137Wn5JUnL4WTwXTzkyUgfBOL3sHNozwfgU03GD/EOUEKqzsIJiz2cbP
|
8
|
+
bFALd4hS+8T4szDLVC9Jl1W6k0CAtmM=
|
9
|
+
-----END EC PRIVATE KEY-----
|