jwt 2.1.0 → 2.2.3
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 +5 -5
- data/.github/workflows/test.yml +74 -0
- data/.gitignore +1 -1
- data/.rspec +1 -0
- data/.rubocop.yml +15 -16
- data/.rubocop_todo.yml +191 -0
- data/{.ebert.yml → .sourcelevel.yml} +1 -1
- data/AUTHORS +101 -0
- data/Appraisals +10 -0
- data/CHANGELOG.md +247 -19
- data/Gemfile +2 -0
- data/README.md +154 -89
- data/Rakefile +4 -1
- data/lib/jwt.rb +9 -42
- data/lib/jwt/algos.rb +44 -0
- data/lib/jwt/algos/ecdsa.rb +1 -1
- data/lib/jwt/algos/hmac.rb +1 -0
- data/lib/jwt/algos/none.rb +15 -0
- data/lib/jwt/algos/ps.rb +43 -0
- data/lib/jwt/algos/unsupported.rb +5 -4
- data/lib/jwt/base64.rb +19 -0
- data/lib/jwt/claims_validator.rb +35 -0
- data/lib/jwt/decode.rb +85 -25
- data/lib/jwt/encode.rb +43 -25
- data/lib/jwt/error.rb +4 -0
- data/lib/jwt/json.rb +18 -0
- data/lib/jwt/jwk.rb +51 -0
- data/lib/jwt/jwk/ec.rb +150 -0
- data/lib/jwt/jwk/hmac.rb +58 -0
- data/lib/jwt/jwk/key_base.rb +18 -0
- data/lib/jwt/jwk/key_finder.rb +62 -0
- data/lib/jwt/jwk/rsa.rb +115 -0
- data/lib/jwt/security_utils.rb +6 -0
- data/lib/jwt/signature.rb +9 -20
- data/lib/jwt/verify.rb +1 -5
- data/lib/jwt/version.rb +2 -2
- data/ruby-jwt.gemspec +4 -7
- metadata +30 -109
- data/.codeclimate.yml +0 -20
- data/.reek.yml +0 -40
- data/.travis.yml +0 -14
- 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/Rakefile
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
require 'bundler/setup'
|
1
2
|
require 'bundler/gem_tasks'
|
2
3
|
|
3
4
|
begin
|
4
5
|
require 'rspec/core/rake_task'
|
6
|
+
require 'rubocop/rake_task'
|
5
7
|
|
6
8
|
RSpec::Core::RakeTask.new(:test)
|
9
|
+
RuboCop::RakeTask.new(:rubocop)
|
7
10
|
|
8
|
-
task default:
|
11
|
+
task default: %i[rubocop test]
|
9
12
|
rescue LoadError
|
10
13
|
puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
|
11
14
|
end
|
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.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt/algos/hmac'
|
4
|
+
require 'jwt/algos/eddsa'
|
5
|
+
require 'jwt/algos/ecdsa'
|
6
|
+
require 'jwt/algos/rsa'
|
7
|
+
require 'jwt/algos/ps'
|
8
|
+
require 'jwt/algos/none'
|
9
|
+
require 'jwt/algos/unsupported'
|
10
|
+
|
11
|
+
# JWT::Signature module
|
12
|
+
module JWT
|
13
|
+
# Signature logic for JWT
|
14
|
+
module Algos
|
15
|
+
extend self
|
16
|
+
|
17
|
+
ALGOS = [
|
18
|
+
Algos::Hmac,
|
19
|
+
Algos::Ecdsa,
|
20
|
+
Algos::Rsa,
|
21
|
+
Algos::Eddsa,
|
22
|
+
Algos::Ps,
|
23
|
+
Algos::None,
|
24
|
+
Algos::Unsupported
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
def find(algorithm)
|
28
|
+
indexed[algorithm && algorithm.downcase]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def indexed
|
34
|
+
@indexed ||= begin
|
35
|
+
fallback = [Algos::Unsupported, nil]
|
36
|
+
ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash|
|
37
|
+
alg.const_get(:SUPPORTED).each do |code|
|
38
|
+
hash[code.downcase] = [alg, code]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/jwt/algos/ecdsa.rb
CHANGED
data/lib/jwt/algos/hmac.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
|
@@ -3,14 +3,15 @@ module JWT
|
|
3
3
|
module Unsupported
|
4
4
|
module_function
|
5
5
|
|
6
|
-
SUPPORTED =
|
7
|
-
def verify(*)
|
8
|
-
raise JWT::VerificationError, 'Algorithm not supported'
|
9
|
-
end
|
6
|
+
SUPPORTED = [].freeze
|
10
7
|
|
11
8
|
def sign(*)
|
12
9
|
raise NotImplementedError, 'Unsupported signing method'
|
13
10
|
end
|
11
|
+
|
12
|
+
def verify(*)
|
13
|
+
raise JWT::VerificationError, 'Algorithm not supported'
|
14
|
+
end
|
14
15
|
end
|
15
16
|
end
|
16
17
|
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,35 @@
|
|
1
|
+
require_relative './error'
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
class ClaimsValidator
|
5
|
+
NUMERIC_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_numeric_claims
|
17
|
+
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def validate_numeric_claims
|
24
|
+
NUMERIC_CLAIMS.each do |claim|
|
25
|
+
validate_is_numeric(claim) if @payload.key?(claim)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_is_numeric(claim)
|
30
|
+
return if @payload[claim].is_a?(Numeric)
|
31
|
+
|
32
|
+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/jwt/decode.rb
CHANGED
@@ -2,47 +2,107 @@
|
|
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
|
+
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
37
|
+
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
38
|
+
|
39
|
+
@key = find_key(&@keyfinder) if @keyfinder
|
40
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
41
|
+
|
42
|
+
Signature.verify(header['alg'], @key, signing_input, @signature)
|
43
|
+
end
|
44
|
+
|
45
|
+
def options_includes_algo_in_header?
|
46
|
+
allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
|
47
|
+
end
|
48
|
+
|
49
|
+
def allowed_algorithms
|
50
|
+
# Order is very important - first check for string keys, next for symbols
|
51
|
+
algos = if @options.key?('algorithm')
|
52
|
+
@options['algorithm']
|
53
|
+
elsif @options.key?(:algorithm)
|
54
|
+
@options[:algorithm]
|
55
|
+
elsif @options.key?('algorithms')
|
56
|
+
@options['algorithms']
|
57
|
+
elsif @options.key?(:algorithms)
|
58
|
+
@options[:algorithms]
|
59
|
+
else
|
60
|
+
[]
|
61
|
+
end
|
62
|
+
Array(algos)
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_key(&keyfinder)
|
66
|
+
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
67
|
+
raise JWT::DecodeError, 'No verification key available' unless key
|
68
|
+
key
|
69
|
+
end
|
70
|
+
|
71
|
+
def verify_claims
|
72
|
+
Verify.verify_claims(payload, @options)
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_segment_count!
|
76
|
+
return if segment_length == 3
|
77
|
+
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
78
|
+
return if segment_length == 2 && header['alg'] == 'none'
|
79
|
+
|
80
|
+
raise(JWT::DecodeError, 'Not enough or too many segments')
|
81
|
+
end
|
82
|
+
|
83
|
+
def segment_length
|
84
|
+
@segments.count
|
85
|
+
end
|
86
|
+
|
87
|
+
def decode_crypto
|
88
|
+
@signature = JWT::Base64.url_decode(@segments[2] || '')
|
89
|
+
end
|
90
|
+
|
91
|
+
def header
|
92
|
+
@header ||= parse_and_decode @segments[0]
|
93
|
+
end
|
94
|
+
|
95
|
+
def payload
|
96
|
+
@payload ||= parse_and_decode @segments[1]
|
97
|
+
end
|
98
|
+
|
99
|
+
def signing_input
|
100
|
+
@segments.first(2).join('.')
|
39
101
|
end
|
40
102
|
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
[header, payload]
|
45
|
-
rescue JSON::ParserError
|
103
|
+
def parse_and_decode(segment)
|
104
|
+
JWT::JSON.parse(JWT::Base64.url_decode(segment))
|
105
|
+
rescue ::JSON::ParserError
|
46
106
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
47
107
|
end
|
48
108
|
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,51 +1,69 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative './algos'
|
4
|
+
require_relative './claims_validator'
|
4
5
|
|
5
6
|
# JWT::Encode module
|
6
7
|
module JWT
|
7
8
|
# Encoding logic for JWT
|
8
9
|
class Encode
|
9
|
-
|
10
|
+
ALG_NONE = 'none'.freeze
|
11
|
+
ALG_KEY = 'alg'.freeze
|
10
12
|
|
11
|
-
def
|
12
|
-
|
13
|
+
def initialize(options)
|
14
|
+
@payload = options[:payload]
|
15
|
+
@key = options[:key]
|
16
|
+
_, @algorithm = Algos.find(options[:algorithm])
|
17
|
+
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
|
13
18
|
end
|
14
19
|
|
15
|
-
def
|
16
|
-
@
|
17
|
-
@key = key
|
18
|
-
@algorithm = algorithm
|
19
|
-
@header_fields = header_fields
|
20
|
-
@segments = encode_segments
|
20
|
+
def segments
|
21
|
+
@segments ||= combine(encoded_header_and_payload, encoded_signature)
|
21
22
|
end
|
22
23
|
|
23
24
|
private
|
24
25
|
|
25
26
|
def encoded_header
|
26
|
-
|
27
|
-
Encode.base64url_encode(JSON.generate(header))
|
27
|
+
@encoded_header ||= encode_header
|
28
28
|
end
|
29
29
|
|
30
30
|
def encoded_payload
|
31
|
-
|
32
|
-
Encode.base64url_encode(JSON.generate(@payload))
|
31
|
+
@encoded_payload ||= encode_payload
|
33
32
|
end
|
34
33
|
|
35
|
-
def encoded_signature
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
34
|
+
def encoded_signature
|
35
|
+
@encoded_signature ||= encode_signature
|
36
|
+
end
|
37
|
+
|
38
|
+
def encoded_header_and_payload
|
39
|
+
@encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
|
40
|
+
end
|
41
|
+
|
42
|
+
def encode_header
|
43
|
+
@headers[ALG_KEY] = @algorithm
|
44
|
+
encode(@headers)
|
45
|
+
end
|
46
|
+
|
47
|
+
def encode_payload
|
48
|
+
if @payload && @payload.is_a?(Hash)
|
49
|
+
ClaimsValidator.new(@payload).validate!
|
41
50
|
end
|
51
|
+
|
52
|
+
encode(@payload)
|
53
|
+
end
|
54
|
+
|
55
|
+
def encode_signature
|
56
|
+
return '' if @algorithm == ALG_NONE
|
57
|
+
|
58
|
+
JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
|
59
|
+
end
|
60
|
+
|
61
|
+
def encode(data)
|
62
|
+
JWT::Base64.url_encode(JWT::JSON.generate(data))
|
42
63
|
end
|
43
64
|
|
44
|
-
def
|
45
|
-
|
46
|
-
payload = encoded_payload
|
47
|
-
signature = encoded_signature([header, payload].join('.'))
|
48
|
-
[header, payload, signature].join('.')
|
65
|
+
def combine(*parts)
|
66
|
+
parts.join('.')
|
49
67
|
end
|
50
68
|
end
|
51
69
|
end
|