jwt 2.1.0 → 2.2.0.pre.beta.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -1
- data/.travis.yml +9 -3
- data/AUTHORS +84 -0
- data/Appraisals +14 -0
- data/CHANGELOG.md +77 -8
- data/README.md +96 -85
- data/lib/jwt.rb +9 -42
- data/lib/jwt/algos/ecdsa.rb +1 -1
- data/lib/jwt/algos/ps.rb +43 -0
- data/lib/jwt/base64.rb +19 -0
- data/lib/jwt/claims_validator.rb +33 -0
- data/lib/jwt/decode.rb +76 -25
- data/lib/jwt/encode.rb +42 -25
- data/lib/jwt/error.rb +16 -12
- data/lib/jwt/json.rb +18 -0
- data/lib/jwt/jwk.rb +31 -0
- data/lib/jwt/jwk/key_finder.rb +57 -0
- data/lib/jwt/jwk/rsa.rb +45 -0
- data/lib/jwt/security_utils.rb +6 -0
- data/lib/jwt/signature.rb +2 -0
- data/lib/jwt/verify.rb +1 -5
- data/lib/jwt/version.rb +3 -3
- data/ruby-jwt.gemspec +6 -3
- metadata +44 -58
- data/.reek.yml +0 -40
- data/Manifest +0 -8
- data/spec/fixtures/certs/ec256-private.pem +0 -8
- data/spec/fixtures/certs/ec256-public.pem +0 -4
- data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
- data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
- data/spec/fixtures/certs/ec384-private.pem +0 -9
- data/spec/fixtures/certs/ec384-public.pem +0 -5
- data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
- data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
- data/spec/fixtures/certs/ec512-private.pem +0 -10
- data/spec/fixtures/certs/ec512-public.pem +0 -6
- data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
- data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
- data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
- data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
- data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
- data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
- data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
- data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
- data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
- data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
- data/spec/integration/readme_examples_spec.rb +0 -202
- data/spec/jwt/verify_spec.rb +0 -232
- data/spec/jwt_spec.rb +0 -315
- data/spec/spec_helper.rb +0 -28
data/lib/jwt.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'base64'
|
3
|
+
require 'jwt/base64'
|
4
|
+
require 'jwt/json'
|
4
5
|
require 'jwt/decode'
|
5
6
|
require 'jwt/default_options'
|
6
7
|
require 'jwt/encode'
|
7
8
|
require 'jwt/error'
|
8
|
-
require 'jwt/
|
9
|
-
require 'jwt/verify'
|
9
|
+
require 'jwt/jwk'
|
10
10
|
|
11
11
|
# JSON Web Token implementation
|
12
12
|
#
|
@@ -18,46 +18,13 @@ module JWT
|
|
18
18
|
module_function
|
19
19
|
|
20
20
|
def encode(payload, key, algorithm = 'HS256', header_fields = {})
|
21
|
-
|
22
|
-
|
21
|
+
Encode.new(payload: payload,
|
22
|
+
key: key,
|
23
|
+
algorithm: algorithm,
|
24
|
+
headers: header_fields).segments
|
23
25
|
end
|
24
26
|
|
25
|
-
def decode(jwt, key = nil, verify = true,
|
26
|
-
|
27
|
-
|
28
|
-
merged_options = DEFAULT_OPTIONS.merge(custom_options)
|
29
|
-
|
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
|
33
|
-
|
34
|
-
Verify.verify_claims(payload, merged_options) if verify
|
35
|
-
|
36
|
-
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
37
|
-
|
38
|
-
[payload, header]
|
39
|
-
end
|
40
|
-
|
41
|
-
def decode_verify_signature(key, header, payload, signature, signing_input, options, &keyfinder)
|
42
|
-
algo, key = signature_algorithm_and_key(header, payload, key, &keyfinder)
|
43
|
-
|
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)
|
46
|
-
|
47
|
-
Signature.verify(algo, key, signing_input, signature)
|
48
|
-
end
|
49
|
-
|
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
|
53
|
-
[header['alg'], key]
|
54
|
-
end
|
55
|
-
|
56
|
-
def allowed_algorithms(options)
|
57
|
-
if options.key?(:algorithm)
|
58
|
-
[options[:algorithm]]
|
59
|
-
else
|
60
|
-
options[:algorithms] || []
|
61
|
-
end
|
27
|
+
def decode(jwt, key = nil, verify = true, options = {}, &keyfinder)
|
28
|
+
Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments
|
62
29
|
end
|
63
30
|
end
|
data/lib/jwt/algos/ecdsa.rb
CHANGED
data/lib/jwt/algos/ps.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module JWT
|
2
|
+
module Algos
|
3
|
+
module Ps
|
4
|
+
# RSASSA-PSS signing algorithms
|
5
|
+
|
6
|
+
module_function
|
7
|
+
|
8
|
+
SUPPORTED = %w[PS256 PS384 PS512].freeze
|
9
|
+
|
10
|
+
def sign(to_sign)
|
11
|
+
require_openssl!
|
12
|
+
|
13
|
+
algorithm, msg, key = to_sign.values
|
14
|
+
|
15
|
+
key_class = key.class
|
16
|
+
|
17
|
+
raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String
|
18
|
+
|
19
|
+
translated_algorithm = algorithm.sub('PS', 'sha')
|
20
|
+
|
21
|
+
key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
|
22
|
+
end
|
23
|
+
|
24
|
+
def verify(to_verify)
|
25
|
+
require_openssl!
|
26
|
+
|
27
|
+
SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
|
28
|
+
end
|
29
|
+
|
30
|
+
def require_openssl!
|
31
|
+
if Object.const_defined?('OpenSSL')
|
32
|
+
major, minor = OpenSSL::VERSION.split('.').first(2)
|
33
|
+
|
34
|
+
unless major.to_i >= 2 && minor.to_i >= 1
|
35
|
+
raise JWT::RequiredDependencyError, "You currently have OpenSSL #{OpenSSL::VERSION}. PS support requires >= 2.1"
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise JWT::RequiredDependencyError, 'PS signing requires OpenSSL +2.1'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/jwt/base64.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
# Base64 helpers
|
7
|
+
class Base64
|
8
|
+
class << self
|
9
|
+
def url_encode(str)
|
10
|
+
::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
|
11
|
+
end
|
12
|
+
|
13
|
+
def url_decode(str)
|
14
|
+
str += '=' * (4 - str.length.modulo(4))
|
15
|
+
::Base64.decode64(str.tr('-_', '+/'))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative './error'
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
class ClaimsValidator
|
5
|
+
INTEGER_CLAIMS = %i[
|
6
|
+
exp
|
7
|
+
iat
|
8
|
+
nbf
|
9
|
+
].freeze
|
10
|
+
|
11
|
+
def initialize(payload)
|
12
|
+
@payload = payload.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
validate_int_claims
|
17
|
+
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def validate_int_claims
|
24
|
+
INTEGER_CLAIMS.each do |claim|
|
25
|
+
validate_is_int(claim) if @payload.key?(claim)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_is_int(claim)
|
30
|
+
raise InvalidPayload, "#{claim} claim must be an Integer but it is a #{@payload[claim].class}" unless @payload[claim].is_a?(Integer)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/jwt/decode.rb
CHANGED
@@ -2,47 +2,98 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
|
5
|
+
require 'jwt/signature'
|
6
|
+
require 'jwt/verify'
|
5
7
|
# JWT::Decode module
|
6
8
|
module JWT
|
7
9
|
# Decoding logic for JWT
|
8
10
|
class Decode
|
9
|
-
|
10
|
-
|
11
|
-
def self.base64url_decode(str)
|
12
|
-
str += '=' * (4 - str.length.modulo(4))
|
13
|
-
Base64.decode64(str.tr('-_', '+/'))
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize(jwt, verify)
|
11
|
+
def initialize(jwt, key, verify, options, &keyfinder)
|
12
|
+
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
|
17
13
|
@jwt = jwt
|
14
|
+
@key = key
|
15
|
+
@options = options
|
16
|
+
@segments = jwt.split('.')
|
18
17
|
@verify = verify
|
19
|
-
@header = ''
|
20
|
-
@payload = ''
|
21
18
|
@signature = ''
|
19
|
+
@keyfinder = keyfinder
|
22
20
|
end
|
23
21
|
|
24
22
|
def decode_segments
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
23
|
+
validate_segment_count!
|
24
|
+
if @verify
|
25
|
+
decode_crypto
|
26
|
+
verify_signature
|
27
|
+
verify_claims
|
28
|
+
end
|
29
|
+
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
30
|
+
[payload, header]
|
30
31
|
end
|
31
32
|
|
32
33
|
private
|
33
34
|
|
34
|
-
def
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
def verify_signature
|
36
|
+
@key = find_key(&@keyfinder) if @keyfinder
|
37
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
38
|
+
|
39
|
+
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
40
|
+
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
41
|
+
|
42
|
+
Signature.verify(header['alg'], @key, signing_input, @signature)
|
43
|
+
end
|
44
|
+
|
45
|
+
def options_includes_algo_in_header?
|
46
|
+
allowed_algorithms.include? header['alg']
|
47
|
+
end
|
48
|
+
|
49
|
+
def allowed_algorithms
|
50
|
+
if @options.key?(:algorithm)
|
51
|
+
[@options[:algorithm]]
|
52
|
+
else
|
53
|
+
@options[:algorithms] || []
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_key(&keyfinder)
|
58
|
+
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
59
|
+
raise JWT::DecodeError, 'No verification key available' unless key
|
60
|
+
key
|
61
|
+
end
|
62
|
+
|
63
|
+
def verify_claims
|
64
|
+
Verify.verify_claims(payload, @options)
|
65
|
+
end
|
66
|
+
|
67
|
+
def validate_segment_count!
|
68
|
+
return if segment_length == 3
|
69
|
+
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
70
|
+
|
71
|
+
raise(JWT::DecodeError, 'Not enough or too many segments')
|
72
|
+
end
|
73
|
+
|
74
|
+
def segment_length
|
75
|
+
@segments.count
|
76
|
+
end
|
77
|
+
|
78
|
+
def decode_crypto
|
79
|
+
@signature = JWT::Base64.url_decode(@segments[2])
|
80
|
+
end
|
81
|
+
|
82
|
+
def header
|
83
|
+
@header ||= parse_and_decode @segments[0]
|
84
|
+
end
|
85
|
+
|
86
|
+
def payload
|
87
|
+
@payload ||= parse_and_decode @segments[1]
|
88
|
+
end
|
89
|
+
|
90
|
+
def signing_input
|
91
|
+
@segments.first(2).join('.')
|
39
92
|
end
|
40
93
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
[header, payload]
|
45
|
-
rescue JSON::ParserError
|
94
|
+
def parse_and_decode(segment)
|
95
|
+
JWT::JSON.parse(JWT::Base64.url_decode(segment))
|
96
|
+
rescue ::JSON::ParserError
|
46
97
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
47
98
|
end
|
48
99
|
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,51 +1,68 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative './claims_validator'
|
4
4
|
|
5
5
|
# JWT::Encode module
|
6
6
|
module JWT
|
7
7
|
# Encoding logic for JWT
|
8
8
|
class Encode
|
9
|
-
|
9
|
+
ALG_NONE = 'none'.freeze
|
10
|
+
ALG_KEY = 'alg'.freeze
|
10
11
|
|
11
|
-
def
|
12
|
-
|
12
|
+
def initialize(options)
|
13
|
+
@payload = options[:payload]
|
14
|
+
@key = options[:key]
|
15
|
+
@algorithm = options[:algorithm]
|
16
|
+
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
|
13
17
|
end
|
14
18
|
|
15
|
-
def
|
16
|
-
@
|
17
|
-
@key = key
|
18
|
-
@algorithm = algorithm
|
19
|
-
@header_fields = header_fields
|
20
|
-
@segments = encode_segments
|
19
|
+
def segments
|
20
|
+
@segments ||= combine(encoded_header_and_payload, encoded_signature)
|
21
21
|
end
|
22
22
|
|
23
23
|
private
|
24
24
|
|
25
25
|
def encoded_header
|
26
|
-
|
27
|
-
Encode.base64url_encode(JSON.generate(header))
|
26
|
+
@encoded_header ||= encode_header
|
28
27
|
end
|
29
28
|
|
30
29
|
def encoded_payload
|
31
|
-
|
32
|
-
Encode.base64url_encode(JSON.generate(@payload))
|
30
|
+
@encoded_payload ||= encode_payload
|
33
31
|
end
|
34
32
|
|
35
|
-
def encoded_signature
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
33
|
+
def encoded_signature
|
34
|
+
@encoded_signature ||= encode_signature
|
35
|
+
end
|
36
|
+
|
37
|
+
def encoded_header_and_payload
|
38
|
+
@encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
|
39
|
+
end
|
40
|
+
|
41
|
+
def encode_header
|
42
|
+
@headers[ALG_KEY] = @algorithm
|
43
|
+
encode(@headers)
|
44
|
+
end
|
45
|
+
|
46
|
+
def encode_payload
|
47
|
+
if @payload && @payload.is_a?(Hash)
|
48
|
+
ClaimsValidator.new(@payload).validate!
|
41
49
|
end
|
50
|
+
|
51
|
+
encode(@payload)
|
52
|
+
end
|
53
|
+
|
54
|
+
def encode_signature
|
55
|
+
return '' if @algorithm == ALG_NONE
|
56
|
+
|
57
|
+
JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
|
58
|
+
end
|
59
|
+
|
60
|
+
def encode(data)
|
61
|
+
JWT::Base64.url_encode(JWT::JSON.generate(data))
|
42
62
|
end
|
43
63
|
|
44
|
-
def
|
45
|
-
|
46
|
-
payload = encoded_payload
|
47
|
-
signature = encoded_signature([header, payload].join('.'))
|
48
|
-
[header, payload, signature].join('.')
|
64
|
+
def combine(*parts)
|
65
|
+
parts.join('.')
|
49
66
|
end
|
50
67
|
end
|
51
68
|
end
|
data/lib/jwt/error.rb
CHANGED
@@ -1,16 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JWT
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
4
|
+
EncodeError = Class.new(StandardError)
|
5
|
+
DecodeError = Class.new(StandardError)
|
6
|
+
RequiredDependencyError = Class.new(StandardError)
|
7
|
+
|
8
|
+
VerificationError = Class.new(DecodeError)
|
9
|
+
ExpiredSignature = Class.new(DecodeError)
|
10
|
+
IncorrectAlgorithm = Class.new(DecodeError)
|
11
|
+
ImmatureSignature = Class.new(DecodeError)
|
12
|
+
InvalidIssuerError = Class.new(DecodeError)
|
13
|
+
InvalidIatError = Class.new(DecodeError)
|
14
|
+
InvalidAudError = Class.new(DecodeError)
|
15
|
+
InvalidSubError = Class.new(DecodeError)
|
16
|
+
InvalidJtiError = Class.new(DecodeError)
|
17
|
+
InvalidPayload = Class.new(DecodeError)
|
18
|
+
|
19
|
+
JWKError = Class.new(DecodeError)
|
16
20
|
end
|
data/lib/jwt/json.rb
ADDED