jwt 1.5.6 → 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 (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