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.
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