jwt 1.5.0 → 2.5.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 (56) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +8 -0
  3. data/.github/workflows/coverage.yml +27 -0
  4. data/.github/workflows/test.yml +67 -0
  5. data/.gitignore +13 -0
  6. data/.reek.yml +22 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +67 -0
  9. data/.sourcelevel.yml +17 -0
  10. data/AUTHORS +119 -0
  11. data/Appraisals +13 -0
  12. data/CHANGELOG.md +786 -0
  13. data/CODE_OF_CONDUCT.md +84 -0
  14. data/CONTRIBUTING.md +99 -0
  15. data/Gemfile +7 -0
  16. data/LICENSE +7 -0
  17. data/README.md +639 -0
  18. data/Rakefile +13 -14
  19. data/lib/jwt/algos/ecdsa.rb +64 -0
  20. data/lib/jwt/algos/eddsa.rb +35 -0
  21. data/lib/jwt/algos/hmac.rb +36 -0
  22. data/lib/jwt/algos/none.rb +17 -0
  23. data/lib/jwt/algos/ps.rb +43 -0
  24. data/lib/jwt/algos/rsa.rb +22 -0
  25. data/lib/jwt/algos/unsupported.rb +19 -0
  26. data/lib/jwt/algos.rb +44 -0
  27. data/lib/jwt/base64.rb +19 -0
  28. data/lib/jwt/claims_validator.rb +37 -0
  29. data/lib/jwt/configuration/container.rb +21 -0
  30. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  31. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  32. data/lib/jwt/configuration.rb +15 -0
  33. data/lib/jwt/decode.rb +145 -0
  34. data/lib/jwt/encode.rb +69 -0
  35. data/lib/jwt/error.rb +22 -0
  36. data/lib/jwt/json.rb +10 -22
  37. data/lib/jwt/jwk/ec.rb +199 -0
  38. data/lib/jwt/jwk/hmac.rb +67 -0
  39. data/lib/jwt/jwk/key_base.rb +35 -0
  40. data/lib/jwt/jwk/key_finder.rb +62 -0
  41. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  42. data/lib/jwt/jwk/rsa.rb +138 -0
  43. data/lib/jwt/jwk/thumbprint.rb +26 -0
  44. data/lib/jwt/jwk.rb +52 -0
  45. data/lib/jwt/security_utils.rb +59 -0
  46. data/lib/jwt/signature.rb +35 -0
  47. data/lib/jwt/verify.rb +113 -0
  48. data/lib/jwt/version.rb +28 -0
  49. data/lib/jwt/x5c_key_finder.rb +55 -0
  50. data/lib/jwt.rb +20 -215
  51. data/ruby-jwt.gemspec +35 -0
  52. metadata +138 -30
  53. data/Manifest +0 -6
  54. data/jwt.gemspec +0 -34
  55. data/spec/helper.rb +0 -2
  56. data/spec/jwt_spec.rb +0 -434
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/security_utils'
4
+ require 'openssl'
5
+ require 'jwt/algos'
6
+ begin
7
+ require 'rbnacl'
8
+ rescue LoadError
9
+ raise if defined?(RbNaCl)
10
+ end
11
+
12
+ # JWT::Signature module
13
+ module JWT
14
+ # Signature logic for JWT
15
+ module Signature
16
+ module_function
17
+
18
+ ToSign = Struct.new(:algorithm, :msg, :key)
19
+ ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
20
+
21
+ def sign(algorithm, msg, key)
22
+ algo, code = Algos.find(algorithm)
23
+ algo.sign ToSign.new(code, msg, key)
24
+ end
25
+
26
+ def verify(algorithm, key, signing_input, signature)
27
+ algo, code = Algos.find(algorithm)
28
+ algo.verify(ToVerify.new(code, key, signing_input, signature))
29
+ rescue OpenSSL::PKey::PKeyError
30
+ raise JWT::VerificationError, 'Signature verification raised'
31
+ ensure
32
+ OpenSSL.errors.clear
33
+ end
34
+ end
35
+ end
data/lib/jwt/verify.rb ADDED
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/error'
4
+
5
+ module JWT
6
+ # JWT verify methods
7
+ class Verify
8
+ DEFAULTS = {
9
+ leeway: 0
10
+ }.freeze
11
+
12
+ class << self
13
+ %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].each do |method_name|
14
+ define_method method_name do |payload, options|
15
+ new(payload, options).send(method_name)
16
+ end
17
+ end
18
+
19
+ def verify_claims(payload, options)
20
+ options.each do |key, val|
21
+ next unless key.to_s =~ /verify/
22
+
23
+ Verify.send(key, payload, options) if val
24
+ end
25
+ end
26
+ end
27
+
28
+ def initialize(payload, options)
29
+ @payload = payload
30
+ @options = DEFAULTS.merge(options)
31
+ end
32
+
33
+ def verify_aud
34
+ return unless (options_aud = @options[:aud])
35
+
36
+ aud = @payload['aud']
37
+ raise(JWT::InvalidAudError, "Invalid audience. Expected #{options_aud}, received #{aud || '<none>'}") if ([*aud] & [*options_aud]).empty?
38
+ end
39
+
40
+ def verify_expiration
41
+ return unless @payload.include?('exp')
42
+ raise(JWT::ExpiredSignature, 'Signature has expired') if @payload['exp'].to_i <= (Time.now.to_i - exp_leeway)
43
+ end
44
+
45
+ def verify_iat
46
+ return unless @payload.include?('iat')
47
+
48
+ iat = @payload['iat']
49
+ raise(JWT::InvalidIatError, 'Invalid iat') if !iat.is_a?(Numeric) || iat.to_f > Time.now.to_f
50
+ end
51
+
52
+ def verify_iss
53
+ return unless (options_iss = @options[:iss])
54
+
55
+ iss = @payload['iss']
56
+
57
+ options_iss = Array(options_iss).map { |item| item.is_a?(Symbol) ? item.to_s : item }
58
+
59
+ case iss
60
+ when *options_iss
61
+ nil
62
+ else
63
+ raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
64
+ end
65
+ end
66
+
67
+ def verify_jti
68
+ options_verify_jti = @options[:verify_jti]
69
+ jti = @payload['jti']
70
+
71
+ if options_verify_jti.respond_to?(:call)
72
+ verified = options_verify_jti.arity == 2 ? options_verify_jti.call(jti, @payload) : options_verify_jti.call(jti)
73
+ raise(JWT::InvalidJtiError, 'Invalid jti') unless verified
74
+ elsif jti.to_s.strip.empty?
75
+ raise(JWT::InvalidJtiError, 'Missing jti')
76
+ end
77
+ end
78
+
79
+ def verify_not_before
80
+ return unless @payload.include?('nbf')
81
+ raise(JWT::ImmatureSignature, 'Signature nbf has not been reached') if @payload['nbf'].to_i > (Time.now.to_i + nbf_leeway)
82
+ end
83
+
84
+ def verify_sub
85
+ return unless (options_sub = @options[:sub])
86
+
87
+ sub = @payload['sub']
88
+ raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
89
+ end
90
+
91
+ def verify_required_claims
92
+ return unless (options_required_claims = @options[:required_claims])
93
+
94
+ options_required_claims.each do |required_claim|
95
+ raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def global_leeway
102
+ @options[:leeway]
103
+ end
104
+
105
+ def exp_leeway
106
+ @options[:exp_leeway] || global_leeway
107
+ end
108
+
109
+ def nbf_leeway
110
+ @options[:nbf_leeway] || global_leeway
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Moments version builder module
4
+ module JWT
5
+ def self.gem_version
6
+ Gem::Version.new VERSION::STRING
7
+ end
8
+
9
+ # Moments version builder module
10
+ module VERSION
11
+ # major version
12
+ MAJOR = 2
13
+ # minor version
14
+ MINOR = 5
15
+ # tiny version
16
+ TINY = 0
17
+ # alpha, beta, etc. tag
18
+ PRE = nil
19
+
20
+ # Build version string
21
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
22
+ end
23
+
24
+ def self.openssl_3?
25
+ return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL')
26
+ return true if OpenSSL::OPENSSL_VERSION_NUMBER >= 3 * 0x10000000
27
+ end
28
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'jwt/error'
5
+
6
+ module JWT
7
+ # If the x5c header certificate chain can be validated by trusted root
8
+ # certificates, and none of the certificates are revoked, returns the public
9
+ # key from the first certificate.
10
+ # See https://tools.ietf.org/html/rfc7515#section-4.1.6
11
+ class X5cKeyFinder
12
+ def initialize(root_certificates, crls = nil)
13
+ raise(ArgumentError, 'Root certificates must be specified') unless root_certificates
14
+
15
+ @store = build_store(root_certificates, crls)
16
+ end
17
+
18
+ def from(x5c_header_or_certificates)
19
+ signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates)
20
+ store_context = OpenSSL::X509::StoreContext.new(@store, signing_certificate, certificate_chain)
21
+
22
+ if store_context.verify
23
+ signing_certificate.public_key
24
+ else
25
+ error = "Certificate verification failed: #{store_context.error_string}."
26
+ if (current_cert = store_context.current_cert)
27
+ error = "#{error} Certificate subject: #{current_cert.subject}."
28
+ end
29
+
30
+ raise(JWT::VerificationError, error)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_store(root_certificates, crls)
37
+ store = OpenSSL::X509::Store.new
38
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
39
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
40
+ root_certificates.each { |certificate| store.add_cert(certificate) }
41
+ crls&.each { |crl| store.add_crl(crl) }
42
+ store
43
+ end
44
+
45
+ def parse_certificates(x5c_header_or_certificates)
46
+ if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) }
47
+ x5c_header_or_certificates
48
+ else
49
+ x5c_header_or_certificates.map do |encoded|
50
+ OpenSSL::X509::Certificate.new(::JWT::Base64.url_decode(encoded))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/jwt.rb CHANGED
@@ -1,226 +1,31 @@
1
- #
2
- # JSON Web Token implementation
3
- #
4
- # Should be up to date with the latest spec:
5
- # http://self-issued.info/docs/draft-jones-json-web-token-06.html
1
+ # frozen_string_literal: true
6
2
 
7
- require 'base64'
8
- require 'openssl'
3
+ require 'jwt/version'
4
+ require 'jwt/base64'
9
5
  require 'jwt/json'
6
+ require 'jwt/decode'
7
+ require 'jwt/configuration'
8
+ require 'jwt/encode'
9
+ require 'jwt/error'
10
+ require 'jwt/jwk'
10
11
 
12
+ # JSON Web Token implementation
13
+ #
14
+ # Should be up to date with the latest spec:
15
+ # https://tools.ietf.org/html/rfc7519
11
16
  module JWT
12
- class DecodeError < StandardError; end
13
- class VerificationError < DecodeError; end
14
- class ExpiredSignature < DecodeError; end
15
- class IncorrectAlgorithm < DecodeError; end
16
- class ImmatureSignature < DecodeError; end
17
- class InvalidIssuerError < DecodeError; end
18
- class InvalidIatError < DecodeError; end
19
- class InvalidAudError < DecodeError; end
20
- class InvalidSubError < DecodeError; end
21
- class InvalidJtiError < DecodeError; end
22
- extend JWT::Json
23
-
24
- NAMED_CURVES = {
25
- 'prime256v1' => 'ES256',
26
- 'secp384r1' => 'ES384',
27
- 'secp521r1' => 'ES512',
28
- }
17
+ extend ::JWT::Configuration
29
18
 
30
19
  module_function
31
20
 
32
- def sign(algorithm, msg, key)
33
- if ['HS256', 'HS384', 'HS512'].include?(algorithm)
34
- sign_hmac(algorithm, msg, key)
35
- elsif ['RS256', 'RS384', 'RS512'].include?(algorithm)
36
- sign_rsa(algorithm, msg, key)
37
- elsif ['ES256', 'ES384', 'ES512'].include?(algorithm)
38
- sign_ecdsa(algorithm, msg, key)
39
- else
40
- raise NotImplementedError.new('Unsupported signing method')
41
- end
42
- end
43
-
44
- def sign_rsa(algorithm, msg, private_key)
45
- private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
46
- end
47
-
48
- def sign_ecdsa(algorithm, msg, private_key)
49
- key_algorithm = NAMED_CURVES[private_key.group.curve_name]
50
- if algorithm != key_algorithm
51
- raise IncorrectAlgorithm.new("payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided")
52
- end
53
-
54
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
55
- private_key.dsa_sign_asn1(digest.digest(msg))
56
- end
57
-
58
- def verify_rsa(algorithm, public_key, signing_input, signature)
59
- public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
60
- end
61
-
62
- def verify_ecdsa(algorithm, public_key, signing_input, signature)
63
- key_algorithm = NAMED_CURVES[public_key.group.curve_name]
64
- if algorithm != key_algorithm
65
- raise IncorrectAlgorithm.new("payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided")
66
- end
67
-
68
- digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha'))
69
- public_key.dsa_verify_asn1(digest.digest(signing_input), signature)
70
- end
71
-
72
- def sign_hmac(algorithm, msg, key)
73
- OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
74
- end
75
-
76
- def base64url_decode(str)
77
- str += '=' * (4 - str.length.modulo(4))
78
- Base64.decode64(str.tr('-_', '+/'))
79
- end
80
-
81
- def base64url_encode(str)
82
- Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
83
- end
84
-
85
- def encoded_header(algorithm='HS256', header_fields={})
86
- header = {'typ' => 'JWT', 'alg' => algorithm}.merge(header_fields)
87
- base64url_encode(encode_json(header))
21
+ def encode(payload, key, algorithm = 'HS256', header_fields = {})
22
+ Encode.new(payload: payload,
23
+ key: key,
24
+ algorithm: algorithm,
25
+ headers: header_fields).segments
88
26
  end
89
27
 
90
- def encoded_payload(payload)
91
- base64url_encode(encode_json(payload))
28
+ def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter
29
+ Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments
92
30
  end
93
-
94
- def encoded_signature(signing_input, key, algorithm)
95
- if algorithm == 'none'
96
- ''
97
- else
98
- signature = sign(algorithm, signing_input, key)
99
- base64url_encode(signature)
100
- end
101
- end
102
-
103
- def encode(payload, key, algorithm='HS256', header_fields={})
104
- algorithm ||= 'none'
105
- segments = []
106
- segments << encoded_header(algorithm, header_fields)
107
- segments << encoded_payload(payload)
108
- segments << encoded_signature(segments.join('.'), key, algorithm)
109
- segments.join('.')
110
- end
111
-
112
- def raw_segments(jwt, verify=true)
113
- segments = jwt.split('.')
114
- required_num_segments = verify ? [3] : [2,3]
115
- raise JWT::DecodeError.new('Not enough or too many segments') unless required_num_segments.include? segments.length
116
- segments
117
- end
118
-
119
- def decode_header_and_payload(header_segment, payload_segment)
120
- header = decode_json(base64url_decode(header_segment))
121
- payload = decode_json(base64url_decode(payload_segment))
122
- [header, payload]
123
- end
124
-
125
- def decoded_segments(jwt, verify=true)
126
- header_segment, payload_segment, crypto_segment = raw_segments(jwt, verify)
127
- header, payload = decode_header_and_payload(header_segment, payload_segment)
128
- signature = base64url_decode(crypto_segment.to_s) if verify
129
- signing_input = [header_segment, payload_segment].join('.')
130
- [header, payload, signature, signing_input]
131
- end
132
-
133
- def decode(jwt, key=nil, verify=true, options={}, &keyfinder)
134
- raise JWT::DecodeError.new('Nil JSON web token') unless jwt
135
-
136
- header, payload, signature, signing_input = decoded_segments(jwt, verify)
137
- raise JWT::DecodeError.new('Not enough or too many segments') unless header && payload
138
-
139
- default_options = {
140
- :verify_expiration => true,
141
- :verify_not_before => true,
142
- :verify_iss => false,
143
- :verify_iat => false,
144
- :verify_jti => false,
145
- :verify_aud => false,
146
- :verify_sub => false,
147
- :leeway => 0
148
- }
149
-
150
- options = default_options.merge(options)
151
-
152
- if verify
153
- algo, key = signature_algorithm_and_key(header, key, &keyfinder)
154
- if options[:algorithm] && algo != options[:algorithm]
155
- raise JWT::IncorrectAlgorithm.new('Expected a different algorithm')
156
- end
157
- verify_signature(algo, key, signing_input, signature)
158
- end
159
-
160
- if options[:verify_expiration] && payload.include?('exp')
161
- raise JWT::ExpiredSignature.new('Signature has expired') unless payload['exp'].to_i > (Time.now.to_i - options[:leeway])
162
- end
163
- if options[:verify_not_before] && payload.include?('nbf')
164
- raise JWT::ImmatureSignature.new('Signature nbf has not been reached') unless payload['nbf'].to_i < (Time.now.to_i + options[:leeway])
165
- end
166
- if options[:verify_iss] && payload.include?('iss')
167
- raise JWT::InvalidIssuerError.new("Invalid issuer. Expected #{options['iss']}, received #{payload['iss']}") unless payload['iss'].to_s == options['iss'].to_s
168
- end
169
- if options[:verify_iat] && payload.include?('iat')
170
- raise JWT::InvalidIatError.new('Invalid iat') unless (payload['iat'].is_a?(Integer) and payload['iat'].to_i <= Time.now.to_i)
171
- end
172
- if options[:verify_aud] && payload.include?('aud')
173
- if payload['aud'].is_a?(Array)
174
- raise JWT::InvalidAudError.new('Invalid audience') unless payload['aud'].include?(options['aud'])
175
- else
176
- raise JWT::InvalidAudError.new("Invalid audience. Expected #{options['aud']}, received #{payload['aud']}") unless payload['aud'].to_s == options['aud'].to_s
177
- end
178
- end
179
- if options[:verify_sub] && payload.include?('sub')
180
- raise JWT::InvalidSubError.new("Invalid subject. Expected #{options['sub']}, received #{payload['sub']}") unless payload['sub'].to_s == options['sub'].to_s
181
- end
182
- if options[:verify_jti] && payload.include?('jti')
183
- raise JWT::InvalidJtiError.new('need iat for verify jwt id') unless payload.include?('iat')
184
- raise JWT::InvalidJtiError.new('Not a uniq jwt id') unless options['jti'].to_s == Digest::MD5.hexdigest("#{key}:#{payload['iat']}")
185
- end
186
-
187
- return payload,header
188
- end
189
-
190
- def signature_algorithm_and_key(header, key, &keyfinder)
191
- if keyfinder
192
- key = keyfinder.call(header)
193
- end
194
- [header['alg'], key]
195
- end
196
-
197
- def verify_signature(algo, key, signing_input, signature)
198
- begin
199
- if ['HS256', 'HS384', 'HS512'].include?(algo)
200
- raise JWT::VerificationError.new('Signature verification failed') unless secure_compare(signature, sign_hmac(algo, signing_input, key))
201
- elsif ['RS256', 'RS384', 'RS512'].include?(algo)
202
- raise JWT::VerificationError.new('Signature verification failed') unless verify_rsa(algo, key, signing_input, signature)
203
- elsif ['ES256', 'ES384', 'ES512'].include?(algo)
204
- raise JWT::VerificationError.new('Signature verification failed') unless verify_ecdsa(algo, key, signing_input, signature)
205
- else
206
- raise JWT::VerificationError.new('Algorithm not supported')
207
- end
208
- rescue OpenSSL::PKey::PKeyError
209
- raise JWT::VerificationError.new('Signature verification failed')
210
- ensure
211
- OpenSSL.errors.clear
212
- end
213
- end
214
-
215
- # From devise
216
- # constant-time comparison algorithm to prevent timing attacks
217
- def secure_compare(a, b)
218
- return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
219
- l = a.unpack "C#{a.bytesize}"
220
-
221
- res = 0
222
- b.each_byte { |byte| res |= byte ^ l.shift }
223
- res == 0
224
- end
225
-
226
31
  end
data/ruby-jwt.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'jwt/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'jwt'
9
+ spec.version = JWT.gem_version
10
+ spec.authors = [
11
+ 'Tim Rudat'
12
+ ]
13
+ spec.email = 'timrudat@gmail.com'
14
+ spec.summary = 'JSON Web Token implementation in Ruby'
15
+ spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.'
16
+ spec.homepage = 'https://github.com/jwt/ruby-jwt'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = '>= 2.5'
19
+ spec.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues',
21
+ 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md"
22
+ }
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) }
25
+ spec.executables = []
26
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
27
+ spec.require_paths = %w[lib]
28
+
29
+ spec.add_development_dependency 'appraisal'
30
+ spec.add_development_dependency 'bundler'
31
+ spec.add_development_dependency 'rake'
32
+ spec.add_development_dependency 'reek'
33
+ spec.add_development_dependency 'rspec'
34
+ spec.add_development_dependency 'simplecov'
35
+ end