jwt 2.1.0 → 2.2.0.pre.beta.0

Sign up to get free protection for your applications and to get access to all the features.
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