jwt 2.1.0 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +74 -0
  3. data/.gitignore +1 -1
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +15 -16
  6. data/.rubocop_todo.yml +191 -0
  7. data/{.ebert.yml → .sourcelevel.yml} +1 -1
  8. data/AUTHORS +101 -0
  9. data/Appraisals +10 -0
  10. data/CHANGELOG.md +247 -19
  11. data/Gemfile +2 -0
  12. data/README.md +154 -89
  13. data/Rakefile +4 -1
  14. data/lib/jwt.rb +9 -42
  15. data/lib/jwt/algos.rb +44 -0
  16. data/lib/jwt/algos/ecdsa.rb +1 -1
  17. data/lib/jwt/algos/hmac.rb +1 -0
  18. data/lib/jwt/algos/none.rb +15 -0
  19. data/lib/jwt/algos/ps.rb +43 -0
  20. data/lib/jwt/algos/unsupported.rb +5 -4
  21. data/lib/jwt/base64.rb +19 -0
  22. data/lib/jwt/claims_validator.rb +35 -0
  23. data/lib/jwt/decode.rb +85 -25
  24. data/lib/jwt/encode.rb +43 -25
  25. data/lib/jwt/error.rb +4 -0
  26. data/lib/jwt/json.rb +18 -0
  27. data/lib/jwt/jwk.rb +51 -0
  28. data/lib/jwt/jwk/ec.rb +150 -0
  29. data/lib/jwt/jwk/hmac.rb +58 -0
  30. data/lib/jwt/jwk/key_base.rb +18 -0
  31. data/lib/jwt/jwk/key_finder.rb +62 -0
  32. data/lib/jwt/jwk/rsa.rb +115 -0
  33. data/lib/jwt/security_utils.rb +6 -0
  34. data/lib/jwt/signature.rb +9 -20
  35. data/lib/jwt/verify.rb +1 -5
  36. data/lib/jwt/version.rb +2 -2
  37. data/ruby-jwt.gemspec +4 -7
  38. metadata +30 -109
  39. data/.codeclimate.yml +0 -20
  40. data/.reek.yml +0 -40
  41. data/.travis.yml +0 -14
  42. data/Manifest +0 -8
  43. data/spec/fixtures/certs/ec256-private.pem +0 -8
  44. data/spec/fixtures/certs/ec256-public.pem +0 -4
  45. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  46. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  47. data/spec/fixtures/certs/ec384-private.pem +0 -9
  48. data/spec/fixtures/certs/ec384-public.pem +0 -5
  49. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  50. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  51. data/spec/fixtures/certs/ec512-private.pem +0 -10
  52. data/spec/fixtures/certs/ec512-public.pem +0 -6
  53. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  54. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  55. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  56. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  57. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  58. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  59. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  60. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  61. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  62. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  63. data/spec/integration/readme_examples_spec.rb +0 -202
  64. data/spec/jwt/verify_spec.rb +0 -232
  65. data/spec/jwt_spec.rb +0 -315
  66. 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: :test
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/signature'
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
- encoder = Encode.new payload, key, algorithm, header_fields
22
- encoder.segments
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, custom_options = {}, &keyfinder)
26
- raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
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
@@ -3,7 +3,7 @@ module JWT
3
3
  module Ecdsa
4
4
  module_function
5
5
 
6
- SUPPORTED = %(ES256 ES384 ES512).freeze
6
+ SUPPORTED = %w[ES256 ES384 ES512].freeze
7
7
  NAMED_CURVES = {
8
8
  'prime256v1' => 'ES256',
9
9
  'secp384r1' => 'ES384',
@@ -7,6 +7,7 @@ module JWT
7
7
 
8
8
  def sign(to_sign)
9
9
  algorithm, msg, key = to_sign.values
10
+ key ||= ''
10
11
  authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
11
12
  if authenticator && padded_key
12
13
  authenticator.auth(padded_key, msg.encode('binary'))
@@ -0,0 +1,15 @@
1
+ module JWT
2
+ module Algos
3
+ module None
4
+ module_function
5
+
6
+ SUPPORTED = %w[none].freeze
7
+
8
+ def sign(*); end
9
+
10
+ def verify(*)
11
+ true
12
+ end
13
+ end
14
+ end
15
+ 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
@@ -3,14 +3,15 @@ module JWT
3
3
  module Unsupported
4
4
  module_function
5
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
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
- 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.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 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
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
- require 'json'
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
- attr_reader :payload, :key, :algorithm, :header_fields, :segments
10
+ ALG_NONE = 'none'.freeze
11
+ ALG_KEY = 'alg'.freeze
10
12
 
11
- def self.base64url_encode(str)
12
- Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
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 initialize(payload, key, algorithm, header_fields)
16
- @payload = payload
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
- header = { 'alg' => @algorithm }.merge(@header_fields)
27
- Encode.base64url_encode(JSON.generate(header))
27
+ @encoded_header ||= encode_header
28
28
  end
29
29
 
30
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))
31
+ @encoded_payload ||= encode_payload
33
32
  end
34
33
 
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)
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 encode_segments
45
- header = encoded_header
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