jwt 1.5.6 → 2.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) 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 +95 -0
  6. data/.rubocop_todo.yml +191 -0
  7. data/.sourcelevel.yml +18 -0
  8. data/AUTHORS +101 -0
  9. data/Appraisals +10 -0
  10. data/CHANGELOG.md +349 -8
  11. data/Gemfile +2 -1
  12. data/README.md +225 -68
  13. data/Rakefile +4 -1
  14. data/lib/jwt.rb +14 -176
  15. data/lib/jwt/algos.rb +44 -0
  16. data/lib/jwt/algos/ecdsa.rb +35 -0
  17. data/lib/jwt/algos/eddsa.rb +23 -0
  18. data/lib/jwt/algos/hmac.rb +34 -0
  19. data/lib/jwt/algos/none.rb +15 -0
  20. data/lib/jwt/algos/ps.rb +43 -0
  21. data/lib/jwt/algos/rsa.rb +19 -0
  22. data/lib/jwt/algos/unsupported.rb +17 -0
  23. data/lib/jwt/base64.rb +19 -0
  24. data/lib/jwt/claims_validator.rb +35 -0
  25. data/lib/jwt/decode.rb +83 -31
  26. data/lib/jwt/default_options.rb +15 -0
  27. data/lib/jwt/encode.rb +69 -0
  28. data/lib/jwt/error.rb +6 -0
  29. data/lib/jwt/json.rb +10 -9
  30. data/lib/jwt/jwk.rb +51 -0
  31. data/lib/jwt/jwk/ec.rb +150 -0
  32. data/lib/jwt/jwk/hmac.rb +58 -0
  33. data/lib/jwt/jwk/key_base.rb +18 -0
  34. data/lib/jwt/jwk/key_finder.rb +62 -0
  35. data/lib/jwt/jwk/rsa.rb +115 -0
  36. data/lib/jwt/security_utils.rb +57 -0
  37. data/lib/jwt/signature.rb +39 -0
  38. data/lib/jwt/verify.rb +45 -53
  39. data/lib/jwt/version.rb +3 -3
  40. data/ruby-jwt.gemspec +6 -8
  41. metadata +39 -95
  42. data/.codeclimate.yml +0 -20
  43. data/.travis.yml +0 -13
  44. data/Manifest +0 -8
  45. data/spec/fixtures/certs/ec256-private.pem +0 -8
  46. data/spec/fixtures/certs/ec256-public.pem +0 -4
  47. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  48. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  49. data/spec/fixtures/certs/ec384-private.pem +0 -9
  50. data/spec/fixtures/certs/ec384-public.pem +0 -5
  51. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  52. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  53. data/spec/fixtures/certs/ec512-private.pem +0 -10
  54. data/spec/fixtures/certs/ec512-public.pem +0 -6
  55. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  56. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  57. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  58. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  59. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  60. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  61. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  62. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  63. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  64. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  65. data/spec/integration/readme_examples_spec.rb +0 -190
  66. data/spec/jwt/verify_spec.rb +0 -197
  67. data/spec/jwt_spec.rb +0 -240
  68. data/spec/spec_helper.rb +0 -31
@@ -0,0 +1,17 @@
1
+ module JWT
2
+ module Algos
3
+ module Unsupported
4
+ module_function
5
+
6
+ SUPPORTED = [].freeze
7
+
8
+ def sign(*)
9
+ raise NotImplementedError, 'Unsupported signing method'
10
+ end
11
+
12
+ def verify(*)
13
+ raise JWT::VerificationError, 'Algorithm not supported'
14
+ end
15
+ end
16
+ end
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
@@ -1,57 +1,109 @@
1
1
  # frozen_string_literal: true
2
- require 'jwt/json'
3
- require 'jwt/verify'
4
2
 
3
+ require 'json'
4
+
5
+ require 'jwt/signature'
6
+ require 'jwt/verify'
5
7
  # JWT::Decode module
6
8
  module JWT
7
- extend JWT::Json
8
-
9
9
  # Decoding logic for JWT
10
10
  class Decode
11
- attr_reader :header, :payload, :signature
12
-
13
11
  def initialize(jwt, key, verify, options, &keyfinder)
12
+ raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
14
13
  @jwt = jwt
15
14
  @key = key
16
- @verify = verify
17
15
  @options = options
16
+ @segments = jwt.split('.')
17
+ @verify = verify
18
+ @signature = ''
18
19
  @keyfinder = keyfinder
19
20
  end
20
21
 
21
22
  def decode_segments
22
- header_segment, payload_segment, crypto_segment = raw_segments(@jwt, @verify)
23
- @header, @payload = decode_header_and_payload(header_segment, payload_segment)
24
- @signature = Decode.base64url_decode(crypto_segment.to_s) if @verify
25
- signing_input = [header_segment, payload_segment].join('.')
26
- [@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]
27
31
  end
28
32
 
29
- def raw_segments(jwt, verify)
30
- segments = jwt.split('.')
31
- required_num_segments = verify ? [3] : [2, 3]
32
- raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length
33
- segments
33
+ private
34
+
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)
34
43
  end
35
- private :raw_segments
36
44
 
37
- def decode_header_and_payload(header_segment, payload_segment)
38
- header = JWT.decode_json(Decode.base64url_decode(header_segment))
39
- payload = JWT.decode_json(Decode.base64url_decode(payload_segment))
40
- [header, payload]
45
+ def options_includes_algo_in_header?
46
+ allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
41
47
  end
42
- private :decode_header_and_payload
43
48
 
44
- def self.base64url_decode(str)
45
- str += '=' * (4 - str.length.modulo(4))
46
- Base64.decode64(str.tr('-_', '+/'))
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)
47
63
  end
48
64
 
49
- def verify
50
- @options.each do |key, val|
51
- next unless key.to_s =~ /verify/
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
52
70
 
53
- Verify.send(key, payload, @options) if val
54
- end
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('.')
101
+ end
102
+
103
+ def parse_and_decode(segment)
104
+ JWT::JSON.parse(JWT::Base64.url_decode(segment))
105
+ rescue ::JSON::ParserError
106
+ raise JWT::DecodeError, 'Invalid segment encoding'
55
107
  end
56
108
  end
57
109
  end
@@ -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
data/lib/jwt/encode.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './algos'
4
+ require_relative './claims_validator'
5
+
6
+ # JWT::Encode module
7
+ module JWT
8
+ # Encoding logic for JWT
9
+ class Encode
10
+ ALG_NONE = 'none'.freeze
11
+ ALG_KEY = 'alg'.freeze
12
+
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 }
18
+ end
19
+
20
+ def segments
21
+ @segments ||= combine(encoded_header_and_payload, encoded_signature)
22
+ end
23
+
24
+ private
25
+
26
+ def encoded_header
27
+ @encoded_header ||= encode_header
28
+ end
29
+
30
+ def encoded_payload
31
+ @encoded_payload ||= encode_payload
32
+ end
33
+
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!
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))
63
+ end
64
+
65
+ def combine(*parts)
66
+ parts.join('.')
67
+ end
68
+ end
69
+ end
data/lib/jwt/error.rb CHANGED
@@ -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
data/lib/jwt/json.rb CHANGED
@@ -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
data/lib/jwt/jwk.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'jwk/key_finder'
4
+
5
+ module JWT
6
+ module JWK
7
+ class << self
8
+ def import(jwk_data)
9
+ jwk_kty = jwk_data[:kty] || jwk_data['kty']
10
+ raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
11
+
12
+ mappings.fetch(jwk_kty.to_s) do |kty|
13
+ raise JWT::JWKError, "Key type #{kty} not supported"
14
+ end.import(jwk_data)
15
+ end
16
+
17
+ def create_from(keypair)
18
+ mappings.fetch(keypair.class) do |klass|
19
+ raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
20
+ end.new(keypair)
21
+ end
22
+
23
+ def classes
24
+ @mappings = nil # reset the cached mappings
25
+ @classes ||= []
26
+ end
27
+
28
+ alias new create_from
29
+
30
+ private
31
+
32
+ def mappings
33
+ @mappings ||= generate_mappings
34
+ end
35
+
36
+ def generate_mappings
37
+ classes.each_with_object({}) do |klass, hash|
38
+ next unless klass.const_defined?('KTYS')
39
+ Array(klass::KTYS).each do |kty|
40
+ hash[kty] = klass
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ require_relative 'jwk/key_base'
49
+ require_relative 'jwk/ec'
50
+ require_relative 'jwk/rsa'
51
+ require_relative 'jwk/hmac'
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ module JWK
7
+ class EC < KeyBase
8
+ extend Forwardable
9
+ def_delegators :@keypair, :public_key
10
+
11
+ KTY = 'EC'.freeze
12
+ KTYS = [KTY, OpenSSL::PKey::EC].freeze
13
+ BINARY = 2
14
+
15
+ def initialize(keypair, kid = nil)
16
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
17
+
18
+ kid ||= generate_kid(keypair)
19
+ super(keypair, kid)
20
+ end
21
+
22
+ def private?
23
+ @keypair.private_key?
24
+ end
25
+
26
+ def export(options = {})
27
+ crv, x_octets, y_octets = keypair_components(keypair)
28
+ exported_hash = {
29
+ kty: KTY,
30
+ crv: crv,
31
+ x: encode_octets(x_octets),
32
+ y: encode_octets(y_octets),
33
+ kid: kid
34
+ }
35
+ return exported_hash unless private? && options[:include_private] == true
36
+
37
+ append_private_parts(exported_hash)
38
+ end
39
+
40
+ private
41
+
42
+ def append_private_parts(the_hash)
43
+ octets = keypair.private_key.to_bn.to_s(BINARY)
44
+ the_hash.merge(
45
+ d: encode_octets(octets)
46
+ )
47
+ end
48
+
49
+ def generate_kid(ec_keypair)
50
+ _crv, x_octets, y_octets = keypair_components(ec_keypair)
51
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
52
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
53
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
54
+ end
55
+
56
+ def keypair_components(ec_keypair)
57
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
58
+ case ec_keypair.group.curve_name
59
+ when 'prime256v1'
60
+ crv = 'P-256'
61
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
62
+ when 'secp384r1'
63
+ crv = 'P-384'
64
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
65
+ when 'secp521r1'
66
+ crv = 'P-521'
67
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
68
+ else
69
+ raise Jwt::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
70
+ end
71
+ [crv, x_octets, y_octets]
72
+ end
73
+
74
+ def encode_octets(octets)
75
+ ::JWT::Base64.url_encode(octets)
76
+ end
77
+
78
+ def encode_open_ssl_bn(key_part)
79
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
80
+ end
81
+
82
+ class << self
83
+ def import(jwk_data)
84
+ # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
85
+ # explanation of the relevant parameters.
86
+
87
+ jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
88
+ raise Jwt::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
89
+
90
+ new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
91
+ end
92
+
93
+ def to_openssl_curve(crv)
94
+ # The JWK specs and OpenSSL use different names for the same curves.
95
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
96
+ # pointers on different names for common curves.
97
+ case crv
98
+ when 'P-256' then 'prime256v1'
99
+ when 'P-384' then 'secp384r1'
100
+ when 'P-521' then 'secp521r1'
101
+ else raise JWT::JWKError, 'Invalid curve provided'
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def jwk_attrs(jwk_data, attrs)
108
+ attrs.map do |attr|
109
+ jwk_data[attr] || jwk_data[attr.to_s]
110
+ end
111
+ end
112
+
113
+ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
114
+ curve = to_openssl_curve(jwk_crv)
115
+
116
+ x_octets = decode_octets(jwk_x)
117
+ y_octets = decode_octets(jwk_y)
118
+
119
+ key = OpenSSL::PKey::EC.new(curve)
120
+
121
+ # The details of the `Point` instantiation are covered in:
122
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
123
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
124
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
125
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
126
+ # Section 2.3.3 of the last of these references specifies that the
127
+ # encoding of an uncompressed point consists of the byte `0x04` followed
128
+ # by the x value then the y value.
129
+ point = OpenSSL::PKey::EC::Point.new(
130
+ OpenSSL::PKey::EC::Group.new(curve),
131
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
132
+ )
133
+
134
+ key.public_key = point
135
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
136
+
137
+ key
138
+ end
139
+
140
+ def decode_octets(jwk_data)
141
+ ::JWT::Base64.url_decode(jwk_data)
142
+ end
143
+
144
+ def decode_open_ssl_bn(jwk_data)
145
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end