jwt 2.0.0 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +5 -5
  2. data/.ebert.yml +2 -1
  3. data/.gitignore +1 -1
  4. data/.travis.yml +18 -3
  5. data/AUTHORS +84 -0
  6. data/Appraisals +18 -0
  7. data/CHANGELOG.md +223 -18
  8. data/README.md +136 -81
  9. data/lib/jwt.rb +9 -40
  10. data/lib/jwt/algos/ecdsa.rb +35 -0
  11. data/lib/jwt/algos/eddsa.rb +23 -0
  12. data/lib/jwt/algos/hmac.rb +34 -0
  13. data/lib/jwt/algos/ps.rb +43 -0
  14. data/lib/jwt/algos/rsa.rb +19 -0
  15. data/lib/jwt/algos/unsupported.rb +16 -0
  16. data/lib/jwt/base64.rb +19 -0
  17. data/lib/jwt/claims_validator.rb +33 -0
  18. data/lib/jwt/decode.rb +83 -25
  19. data/lib/jwt/default_options.rb +2 -1
  20. data/lib/jwt/encode.rb +42 -25
  21. data/lib/jwt/error.rb +4 -0
  22. data/lib/jwt/json.rb +18 -0
  23. data/lib/jwt/jwk.rb +31 -0
  24. data/lib/jwt/jwk/key_finder.rb +57 -0
  25. data/lib/jwt/jwk/rsa.rb +54 -0
  26. data/lib/jwt/security_utils.rb +6 -1
  27. data/lib/jwt/signature.rb +27 -79
  28. data/lib/jwt/verify.rb +5 -8
  29. data/lib/jwt/version.rb +2 -2
  30. data/ruby-jwt.gemspec +7 -4
  31. metadata +54 -63
  32. data/.reek.yml +0 -40
  33. data/Manifest +0 -8
  34. data/spec/fixtures/certs/ec256-private.pem +0 -8
  35. data/spec/fixtures/certs/ec256-public.pem +0 -4
  36. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  37. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  38. data/spec/fixtures/certs/ec384-private.pem +0 -9
  39. data/spec/fixtures/certs/ec384-public.pem +0 -5
  40. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  41. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  42. data/spec/fixtures/certs/ec512-private.pem +0 -10
  43. data/spec/fixtures/certs/ec512-public.pem +0 -6
  44. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  45. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  46. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  47. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  48. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  49. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  50. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  51. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  52. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  53. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  54. data/spec/integration/readme_examples_spec.rb +0 -202
  55. data/spec/jwt/verify_spec.rb +0 -219
  56. data/spec/jwt_spec.rb +0 -257
  57. data/spec/spec_helper.rb +0 -28
@@ -0,0 +1,35 @@
1
+ module JWT
2
+ module Algos
3
+ module Ecdsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[ES256 ES384 ES512].freeze
7
+ NAMED_CURVES = {
8
+ 'prime256v1' => 'ES256',
9
+ 'secp384r1' => 'ES384',
10
+ 'secp521r1' => 'ES512'
11
+ }.freeze
12
+
13
+ def sign(to_sign)
14
+ algorithm, msg, key = to_sign.values
15
+ key_algorithm = NAMED_CURVES[key.group.curve_name]
16
+ if algorithm != key_algorithm
17
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
18
+ end
19
+
20
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
21
+ SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
22
+ end
23
+
24
+ def verify(to_verify)
25
+ algorithm, public_key, signing_input, signature = to_verify.values
26
+ key_algorithm = NAMED_CURVES[public_key.group.curve_name]
27
+ if algorithm != key_algorithm
28
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
29
+ end
30
+ digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
31
+ public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ module JWT
2
+ module Algos
3
+ module Eddsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[ED25519].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided" if algorithm.downcase.to_sym != key.primitive
12
+ key.sign(msg)
13
+ end
14
+
15
+ def verify(to_verify)
16
+ algorithm, public_key, signing_input, signature = to_verify.values
17
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive
18
+ raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
19
+ public_key.verify(signature, signing_input)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,34 @@
1
+ module JWT
2
+ module Algos
3
+ module Hmac
4
+ module_function
5
+
6
+ SUPPORTED = %w[HS256 HS512256 HS384 HS512].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ key ||= ''
11
+ authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
12
+ if authenticator && padded_key
13
+ authenticator.auth(padded_key, msg.encode('binary'))
14
+ else
15
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
16
+ end
17
+ end
18
+
19
+ def verify(to_verify)
20
+ algorithm, public_key, signing_input, signature = to_verify.values
21
+ authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
22
+ if authenticator && padded_key
23
+ begin
24
+ authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary'))
25
+ rescue RbNaCl::BadAuthenticatorError
26
+ false
27
+ end
28
+ else
29
+ SecurityUtils.secure_compare(signature, sign(JWT::Signature::ToSign.new(algorithm, signing_input, public_key)))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -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
@@ -0,0 +1,19 @@
1
+ module JWT
2
+ module Algos
3
+ module Rsa
4
+ module_function
5
+
6
+ SUPPORTED = %w[RS256 RS384 RS512].freeze
7
+
8
+ def sign(to_sign)
9
+ algorithm, msg, key = to_sign.values
10
+ raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.class == String
11
+ key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
12
+ end
13
+
14
+ def verify(to_verify)
15
+ SecurityUtils.verify_rsa(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module JWT
2
+ module Algos
3
+ module Unsupported
4
+ module_function
5
+
6
+ SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } }
7
+ def verify(*)
8
+ raise JWT::VerificationError, 'Algorithm not supported'
9
+ end
10
+
11
+ def sign(*)
12
+ raise NotImplementedError, 'Unsupported signing method'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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
@@ -2,47 +2,105 @@
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
- attr_reader :header, :payload, :signature
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
- header_segment, payload_segment, crypto_segment = raw_segments
26
- @header, @payload = decode_header_and_payload(header_segment, payload_segment)
27
- @signature = Decode.base64url_decode(crypto_segment.to_s) if @verify
28
- signing_input = [header_segment, payload_segment].join('.')
29
- [@header, @payload, @signature, signing_input]
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 raw_segments
35
- segments = @jwt.split('.')
36
- required_num_segments = @verify ? [3] : [2, 3]
37
- raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length
38
- segments
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.include? header['alg']
47
+ end
48
+
49
+ def allowed_algorithms
50
+ # Order is very important - first check for string keys, next for symbols
51
+ 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
+ end
63
+
64
+ def find_key(&keyfinder)
65
+ key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
66
+ raise JWT::DecodeError, 'No verification key available' unless key
67
+ key
68
+ end
69
+
70
+ def verify_claims
71
+ Verify.verify_claims(payload, @options)
72
+ end
73
+
74
+ def validate_segment_count!
75
+ return if segment_length == 3
76
+ return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
77
+
78
+ raise(JWT::DecodeError, 'Not enough or too many segments')
79
+ end
80
+
81
+ def segment_length
82
+ @segments.count
83
+ end
84
+
85
+ def decode_crypto
86
+ @signature = JWT::Base64.url_decode(@segments[2])
87
+ end
88
+
89
+ def header
90
+ @header ||= parse_and_decode @segments[0]
91
+ end
92
+
93
+ def payload
94
+ @payload ||= parse_and_decode @segments[1]
95
+ end
96
+
97
+ def signing_input
98
+ @segments.first(2).join('.')
39
99
  end
40
100
 
41
- def decode_header_and_payload(header_segment, payload_segment)
42
- header = JSON.parse(Decode.base64url_decode(header_segment))
43
- payload = JSON.parse(Decode.base64url_decode(payload_segment))
44
- [header, payload]
45
- rescue JSON::ParserError
101
+ def parse_and_decode(segment)
102
+ JWT::JSON.parse(JWT::Base64.url_decode(segment))
103
+ rescue ::JSON::ParserError
46
104
  raise JWT::DecodeError, 'Invalid segment encoding'
47
105
  end
48
106
  end
@@ -8,7 +8,8 @@ module JWT
8
8
  verify_jti: false,
9
9
  verify_aud: false,
10
10
  verify_sub: false,
11
- leeway: 0
11
+ leeway: 0,
12
+ algorithms: ['HS256']
12
13
  }.freeze
13
14
  end
14
15
  end
@@ -1,51 +1,68 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
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
- attr_reader :payload, :key, :algorithm, :header_fields, :segments
9
+ ALG_NONE = 'none'.freeze
10
+ ALG_KEY = 'alg'.freeze
10
11
 
11
- def self.base64url_encode(str)
12
- Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
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 initialize(payload, key, algorithm, header_fields)
16
- @payload = payload
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
- header = { 'alg' => @algorithm }.merge(@header_fields)
27
- Encode.base64url_encode(JSON.generate(header))
26
+ @encoded_header ||= encode_header
28
27
  end
29
28
 
30
29
  def encoded_payload
31
- raise InvalidPayload, 'exp claim must be an integer' if @payload && !@payload.is_a?(Array) && @payload.key?('exp') && !@payload['exp'].is_a?(Integer)
32
- Encode.base64url_encode(JSON.generate(@payload))
30
+ @encoded_payload ||= encode_payload
33
31
  end
34
32
 
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)
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 encode_segments
45
- header = encoded_header
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