jwt 2.1.0 → 2.2.0.pre.beta.0

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 (51) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -1
  3. data/.travis.yml +9 -3
  4. data/AUTHORS +84 -0
  5. data/Appraisals +14 -0
  6. data/CHANGELOG.md +77 -8
  7. data/README.md +96 -85
  8. data/lib/jwt.rb +9 -42
  9. data/lib/jwt/algos/ecdsa.rb +1 -1
  10. data/lib/jwt/algos/ps.rb +43 -0
  11. data/lib/jwt/base64.rb +19 -0
  12. data/lib/jwt/claims_validator.rb +33 -0
  13. data/lib/jwt/decode.rb +76 -25
  14. data/lib/jwt/encode.rb +42 -25
  15. data/lib/jwt/error.rb +16 -12
  16. data/lib/jwt/json.rb +18 -0
  17. data/lib/jwt/jwk.rb +31 -0
  18. data/lib/jwt/jwk/key_finder.rb +57 -0
  19. data/lib/jwt/jwk/rsa.rb +45 -0
  20. data/lib/jwt/security_utils.rb +6 -0
  21. data/lib/jwt/signature.rb +2 -0
  22. data/lib/jwt/verify.rb +1 -5
  23. data/lib/jwt/version.rb +3 -3
  24. data/ruby-jwt.gemspec +6 -3
  25. metadata +44 -58
  26. data/.reek.yml +0 -40
  27. data/Manifest +0 -8
  28. data/spec/fixtures/certs/ec256-private.pem +0 -8
  29. data/spec/fixtures/certs/ec256-public.pem +0 -4
  30. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  31. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  32. data/spec/fixtures/certs/ec384-private.pem +0 -9
  33. data/spec/fixtures/certs/ec384-public.pem +0 -5
  34. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  35. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  36. data/spec/fixtures/certs/ec512-private.pem +0 -10
  37. data/spec/fixtures/certs/ec512-public.pem +0 -6
  38. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  39. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  40. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  41. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  42. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  43. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  44. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  45. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  46. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  47. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  48. data/spec/integration/readme_examples_spec.rb +0 -202
  49. data/spec/jwt/verify_spec.rb +0 -232
  50. data/spec/jwt_spec.rb +0 -315
  51. data/spec/spec_helper.rb +0 -28
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
@@ -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',
@@ -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
+ # 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,98 @@
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
+ @key = find_key(&@keyfinder) if @keyfinder
37
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
38
+
39
+ raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
40
+ raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
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
+ if @options.key?(:algorithm)
51
+ [@options[:algorithm]]
52
+ else
53
+ @options[:algorithms] || []
54
+ end
55
+ end
56
+
57
+ def find_key(&keyfinder)
58
+ key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
59
+ raise JWT::DecodeError, 'No verification key available' unless key
60
+ key
61
+ end
62
+
63
+ def verify_claims
64
+ Verify.verify_claims(payload, @options)
65
+ end
66
+
67
+ def validate_segment_count!
68
+ return if segment_length == 3
69
+ return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
70
+
71
+ raise(JWT::DecodeError, 'Not enough or too many segments')
72
+ end
73
+
74
+ def segment_length
75
+ @segments.count
76
+ end
77
+
78
+ def decode_crypto
79
+ @signature = JWT::Base64.url_decode(@segments[2])
80
+ end
81
+
82
+ def header
83
+ @header ||= parse_and_decode @segments[0]
84
+ end
85
+
86
+ def payload
87
+ @payload ||= parse_and_decode @segments[1]
88
+ end
89
+
90
+ def signing_input
91
+ @segments.first(2).join('.')
39
92
  end
40
93
 
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
94
+ def parse_and_decode(segment)
95
+ JWT::JSON.parse(JWT::Base64.url_decode(segment))
96
+ rescue ::JSON::ParserError
46
97
  raise JWT::DecodeError, 'Invalid segment encoding'
47
98
  end
48
99
  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?(Hash) && @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
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
- class EncodeError < StandardError; end
5
- class DecodeError < StandardError; end
6
- class VerificationError < DecodeError; end
7
- class ExpiredSignature < DecodeError; end
8
- class IncorrectAlgorithm < DecodeError; end
9
- class ImmatureSignature < DecodeError; end
10
- class InvalidIssuerError < DecodeError; end
11
- class InvalidIatError < DecodeError; end
12
- class InvalidAudError < DecodeError; end
13
- class InvalidSubError < DecodeError; end
14
- class InvalidJtiError < DecodeError; end
15
- class InvalidPayload < DecodeError; end
4
+ EncodeError = Class.new(StandardError)
5
+ DecodeError = Class.new(StandardError)
6
+ RequiredDependencyError = Class.new(StandardError)
7
+
8
+ VerificationError = Class.new(DecodeError)
9
+ ExpiredSignature = Class.new(DecodeError)
10
+ IncorrectAlgorithm = Class.new(DecodeError)
11
+ ImmatureSignature = Class.new(DecodeError)
12
+ InvalidIssuerError = Class.new(DecodeError)
13
+ InvalidIatError = Class.new(DecodeError)
14
+ InvalidAudError = Class.new(DecodeError)
15
+ InvalidSubError = Class.new(DecodeError)
16
+ InvalidJtiError = Class.new(DecodeError)
17
+ InvalidPayload = Class.new(DecodeError)
18
+
19
+ JWKError = Class.new(DecodeError)
16
20
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module JWT
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
12
+
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
16
+ end
17
+ end
18
+ end