jwt 1.5.6 → 2.2.2

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.
Files changed (58) hide show
  1. checksums.yaml +5 -5
  2. data/.ebert.yml +18 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +96 -0
  5. data/.travis.yml +26 -10
  6. data/AUTHORS +84 -0
  7. data/Appraisals +18 -0
  8. data/CHANGELOG.md +296 -10
  9. data/Gemfile +0 -1
  10. data/README.md +182 -64
  11. data/lib/jwt.rb +14 -176
  12. data/lib/jwt/algos/ecdsa.rb +35 -0
  13. data/lib/jwt/algos/eddsa.rb +23 -0
  14. data/lib/jwt/algos/hmac.rb +34 -0
  15. data/lib/jwt/algos/ps.rb +43 -0
  16. data/lib/jwt/algos/rsa.rb +19 -0
  17. data/lib/jwt/algos/unsupported.rb +16 -0
  18. data/lib/jwt/base64.rb +19 -0
  19. data/lib/jwt/claims_validator.rb +33 -0
  20. data/lib/jwt/decode.rb +81 -31
  21. data/lib/jwt/default_options.rb +15 -0
  22. data/lib/jwt/encode.rb +68 -0
  23. data/lib/jwt/error.rb +6 -0
  24. data/lib/jwt/json.rb +10 -9
  25. data/lib/jwt/jwk.rb +31 -0
  26. data/lib/jwt/jwk/key_finder.rb +57 -0
  27. data/lib/jwt/jwk/rsa.rb +54 -0
  28. data/lib/jwt/security_utils.rb +57 -0
  29. data/lib/jwt/signature.rb +54 -0
  30. data/lib/jwt/verify.rb +45 -53
  31. data/lib/jwt/version.rb +3 -3
  32. data/ruby-jwt.gemspec +11 -7
  33. metadata +76 -67
  34. data/Manifest +0 -8
  35. data/spec/fixtures/certs/ec256-private.pem +0 -8
  36. data/spec/fixtures/certs/ec256-public.pem +0 -4
  37. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  38. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  39. data/spec/fixtures/certs/ec384-private.pem +0 -9
  40. data/spec/fixtures/certs/ec384-public.pem +0 -5
  41. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  42. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  43. data/spec/fixtures/certs/ec512-private.pem +0 -10
  44. data/spec/fixtures/certs/ec512-public.pem +0 -6
  45. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  46. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  47. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  48. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  49. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  50. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  51. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  52. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  53. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  54. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  55. data/spec/integration/readme_examples_spec.rb +0 -190
  56. data/spec/jwt/verify_spec.rb +0 -197
  57. data/spec/jwt_spec.rb +0 -240
  58. data/spec/spec_helper.rb +0 -31
@@ -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
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './claims_validator'
4
+
5
+ # JWT::Encode module
6
+ module JWT
7
+ # Encoding logic for JWT
8
+ class Encode
9
+ ALG_NONE = 'none'.freeze
10
+ ALG_KEY = 'alg'.freeze
11
+
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 }
17
+ end
18
+
19
+ def segments
20
+ @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
+ end
22
+
23
+ private
24
+
25
+ def encoded_header
26
+ @encoded_header ||= encode_header
27
+ end
28
+
29
+ def encoded_payload
30
+ @encoded_payload ||= encode_payload
31
+ end
32
+
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!
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))
62
+ end
63
+
64
+ def combine(*parts)
65
+ parts.join('.')
66
+ end
67
+ end
68
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module JWT
4
+ class EncodeError < StandardError; end
3
5
  class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
+
4
8
  class VerificationError < DecodeError; end
5
9
  class ExpiredSignature < DecodeError; end
6
10
  class IncorrectAlgorithm < DecodeError; end
@@ -11,4 +15,6 @@ module JWT
11
15
  class InvalidSubError < DecodeError; end
12
16
  class InvalidJtiError < DecodeError; end
13
17
  class InvalidPayload < DecodeError; end
18
+
19
+ class JWKError < DecodeError; end
14
20
  end
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'json'
3
4
 
4
5
  module JWT
5
- # JSON fallback implementation or ruby 1.8.x
6
- module Json
7
- def decode_json(encoded)
8
- JSON.parse(encoded)
9
- rescue JSON::ParserError
10
- raise JWT::DecodeError, 'Invalid segment encoding'
11
- end
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
12
12
 
13
- def encode_json(raw)
14
- JSON.generate(raw)
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
15
16
  end
16
17
  end
17
18
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jwk/rsa'
4
+ require_relative 'jwk/key_finder'
5
+
6
+ module JWT
7
+ module JWK
8
+ MAPPINGS = {
9
+ 'RSA' => ::JWT::JWK::RSA,
10
+ OpenSSL::PKey::RSA => ::JWT::JWK::RSA
11
+ }.freeze
12
+
13
+ class << self
14
+ def import(jwk_data)
15
+ raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_data[:kty]
16
+
17
+ MAPPINGS.fetch(jwk_data[:kty].to_s) do |kty|
18
+ raise JWT::JWKError, "Key type #{kty} not supported"
19
+ end.import(jwk_data)
20
+ end
21
+
22
+ def create_from(keypair)
23
+ MAPPINGS.fetch(keypair.class) do |klass|
24
+ raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
25
+ end.new(keypair)
26
+ end
27
+
28
+ alias new create_from
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KeyFinder
6
+ def initialize(options)
7
+ jwks_or_loader = options[:jwks]
8
+ @jwks = jwks_or_loader if jwks_or_loader.is_a?(Hash)
9
+ @jwk_loader = jwks_or_loader if jwks_or_loader.respond_to?(:call)
10
+ end
11
+
12
+ def key_for(kid)
13
+ raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
14
+
15
+ jwk = resolve_key(kid)
16
+
17
+ raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
18
+
19
+ ::JWT::JWK.import(jwk).keypair
20
+ end
21
+
22
+ private
23
+
24
+ def resolve_key(kid)
25
+ jwk = find_key(kid)
26
+
27
+ return jwk if jwk
28
+
29
+ if reloadable?
30
+ load_keys(invalidate: true)
31
+ return find_key(kid)
32
+ end
33
+
34
+ nil
35
+ end
36
+
37
+ def jwks
38
+ return @jwks if @jwks
39
+
40
+ load_keys
41
+ @jwks
42
+ end
43
+
44
+ def load_keys(opts = {})
45
+ @jwks = @jwk_loader.call(opts)
46
+ end
47
+
48
+ def find_key(kid)
49
+ Array(jwks[:keys]).find { |key| key[:kid] == kid }
50
+ end
51
+
52
+ def reloadable?
53
+ @jwk_loader
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class RSA
6
+ attr_reader :keypair
7
+
8
+ BINARY = 2
9
+ KTY = 'RSA'.freeze
10
+
11
+ def initialize(keypair)
12
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
+
14
+ @keypair = keypair
15
+ end
16
+
17
+ def private?
18
+ keypair.private?
19
+ end
20
+
21
+ def public_key
22
+ keypair.public_key
23
+ end
24
+
25
+ def kid
26
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
27
+ OpenSSL::ASN1::Integer.new(public_key.e)])
28
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
29
+ end
30
+
31
+ def export
32
+ {
33
+ kty: KTY,
34
+ n: ::Base64.urlsafe_encode64(public_key.n.to_s(BINARY), padding: false),
35
+ e: ::Base64.urlsafe_encode64(public_key.e.to_s(BINARY), padding: false),
36
+ kid: kid
37
+ }
38
+ end
39
+
40
+ def self.import(jwk_data)
41
+ imported_key = OpenSSL::PKey::RSA.new
42
+ if imported_key.respond_to?(:set_key)
43
+ imported_key.set_key(OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY),
44
+ OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY),
45
+ nil)
46
+ else
47
+ imported_key.n = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY)
48
+ imported_key.e = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY)
49
+ end
50
+ self.new(imported_key)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
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 verify_ps(algorithm, public_key, signing_input, signature)
24
+ formatted_algorithm = algorithm.sub('PS', 'sha')
25
+
26
+ public_key.verify_pss(formatted_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: formatted_algorithm)
27
+ end
28
+
29
+ def asn1_to_raw(signature, public_key)
30
+ byte_size = (public_key.group.degree + 7) / 8
31
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
32
+ end
33
+
34
+ def raw_to_asn1(signature, private_key)
35
+ byte_size = (private_key.group.degree + 7) / 8
36
+ sig_bytes = signature[0..(byte_size - 1)]
37
+ sig_char = signature[byte_size..-1] || ''
38
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
39
+ end
40
+
41
+ def rbnacl_fixup(algorithm, key)
42
+ algorithm = algorithm.sub('HS', 'SHA').to_sym
43
+
44
+ return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
45
+
46
+ authenticator = RbNaCl::HMAC.const_get(algorithm)
47
+
48
+ # Fall back to OpenSSL for keys larger than 32 bytes.
49
+ return [] if key.bytesize > authenticator.key_bytes
50
+
51
+ [
52
+ authenticator,
53
+ key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
54
+ ]
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,54 @@
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/ps'
10
+ require 'jwt/algos/unsupported'
11
+ begin
12
+ require 'rbnacl'
13
+ rescue LoadError
14
+ raise if defined?(RbNaCl)
15
+ end
16
+
17
+ # JWT::Signature module
18
+ module JWT
19
+ # Signature logic for JWT
20
+ module Signature
21
+ extend self
22
+ ALGOS = [
23
+ Algos::Hmac,
24
+ Algos::Ecdsa,
25
+ Algos::Rsa,
26
+ Algos::Eddsa,
27
+ Algos::Ps,
28
+ Algos::Unsupported
29
+ ].freeze
30
+ ToSign = Struct.new(:algorithm, :msg, :key)
31
+ ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
32
+
33
+ def sign(algorithm, msg, key)
34
+ algo = ALGOS.find do |alg|
35
+ alg.const_get(:SUPPORTED).include? algorithm
36
+ end
37
+ algo.sign ToSign.new(algorithm, msg, key)
38
+ end
39
+
40
+ def verify(algorithm, key, signing_input, signature)
41
+ raise JWT::DecodeError, 'No verification key available' unless key
42
+
43
+ algo = ALGOS.find do |alg|
44
+ alg.const_get(:SUPPORTED).include? algorithm
45
+ end
46
+ verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature))
47
+ raise(JWT::VerificationError, 'Signature verification raised') unless verified
48
+ rescue OpenSSL::PKey::PKeyError
49
+ raise JWT::VerificationError, 'Signature verification raised'
50
+ ensure
51
+ OpenSSL.errors.clear
52
+ end
53
+ end
54
+ end
@@ -1,106 +1,98 @@
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(verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub).each do |method_name|
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 = extract_option(: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
- def verify_aud_array(audience, options_aud)
34
- if options_aud.is_a?(Array)
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
- if !@payload['iat'].is_a?(Numeric) || @payload['iat'].to_f > (Time.now.to_f + leeway)
55
- raise(JWT::InvalidIatError, 'Invalid iat')
56
- end
47
+ iat = @payload['iat']
48
+ raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > Time.now.to_f
57
49
  end
58
50
 
59
51
  def verify_iss
60
- return unless (options_iss = extract_option(:iss))
52
+ return unless (options_iss = @options[:iss])
61
53
 
62
- if @payload['iss'].to_s != options_iss.to_s
63
- raise(
64
- JWT::InvalidIssuerError,
65
- "Invalid issuer. Expected #{options_iss}, received #{@payload['iss'] || '<none>'}"
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 = extract_option(: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(@payload['jti'])
74
- else
75
- raise(JWT::InvalidJtiError, 'Missing jti') if @payload['jti'].to_s.strip.empty?
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')
76
70
  end
77
71
  end
78
72
 
79
73
  def verify_not_before
80
74
  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
75
+ raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
85
76
  end
86
77
 
87
78
  def verify_sub
88
- return unless (options_sub = extract_option(: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
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
94
82
  end
95
83
 
96
84
  private
97
85
 
98
- def extract_option(key)
99
- @options.values_at(key.to_sym, key.to_s).compact.first
86
+ def global_leeway
87
+ @options[:leeway]
88
+ end
89
+
90
+ def exp_leeway
91
+ @options[:exp_leeway] || global_leeway
100
92
  end
101
93
 
102
- def leeway
103
- extract_option :leeway
94
+ def nbf_leeway
95
+ @options[:nbf_leeway] || global_leeway
104
96
  end
105
97
  end
106
98
  end