omniauth_openid_connect 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7b5060fd9a0536e2740b340c56459f76b628912dea1ad54da415c2453921e6e3
4
- data.tar.gz: ada9bd68c69837538cd485ce0595525b2109feb0f79611a3ceb06fa1bf7eed96
3
+ metadata.gz: d5ccd20bf84d71220597692301b64b1c3b0f8bf19faf1d788a6f1185bf42bfcb
4
+ data.tar.gz: 9336782a2449bfdefc07521f79bcd8030bf4cc2a6f094a01d9613dbcc21727f9
5
5
  SHA512:
6
- metadata.gz: 24c424fb608e45beba966b6914accdfad6e5af277c9428c94d3aec4ab3f4c010bfa66117a42d6ad1f57de9ea6d6d3ff99eaa5a33bdcbc753d1b7db568f3e870b
7
- data.tar.gz: f2ebfcd68b35425cc656ecaa58151b962d0bc96a4023f1bfcf312e363e2e0878c298bc90d7fc70104220b71cdbc77346b3f64c4653222b8df1110a12bb249dd0
6
+ metadata.gz: 07b23acf7a852a2b5a1d7a4b6852108562fc7ce2c7f00ead2fe91989f00d736643c8e9f510ff8029956a0e55006d70b4671006ed4d2ca8a5c9a74f641a0c0e60
7
+ data.tar.gz: e22f6c174179f08acf046f05a50da341a01d576353a929fca9bd4c49f6d5ceaa5a761ce7ddaa4cfa6dae7470923bd0a646d634f6b7f563dca93df436c9b60a05
@@ -9,12 +9,12 @@ on:
9
9
  types: [opened, synchronize, reopened]
10
10
 
11
11
  jobs:
12
- base:
12
+ test:
13
13
  runs-on: ubuntu-latest
14
14
  strategy:
15
15
  fail-fast: false
16
16
  matrix:
17
- ruby: ["2.5", "2.6", "2.7", "3.0"]
17
+ ruby: ["2.5", "2.6", "2.7", "3.0", "3.1"]
18
18
  name: Ruby ${{ matrix.ruby }}
19
19
 
20
20
  steps:
@@ -29,3 +29,35 @@ jobs:
29
29
 
30
30
  - name: Run tests
31
31
  run: bundle exec rake
32
+
33
+ - name: Coveralls Parallel
34
+ uses: coverallsapp/github-action@master
35
+ with:
36
+ github-token: ${{ secrets.github_token }}
37
+ flag-name: ruby-${{ matrix.ruby }}
38
+ parallel: true
39
+
40
+ finish:
41
+ needs: test
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - name: Coveralls Finished
45
+ uses: coverallsapp/github-action@master
46
+ with:
47
+ github-token: ${{ secrets.github_token }}
48
+ parallel-finished: true
49
+
50
+ rubocop:
51
+ runs-on: ubuntu-latest
52
+ steps:
53
+ - name: Checkout code
54
+ uses: actions/checkout@v2
55
+
56
+ - name: Setup Ruby
57
+ uses: ruby/setup-ruby@v1
58
+ with:
59
+ bundler-cache: true
60
+ ruby-version: "2.7"
61
+
62
+ - name: rubocop
63
+ run: bundle exec rubocop --parallel
data/.rubocop.yml CHANGED
@@ -58,7 +58,4 @@ Metrics/MethodLength:
58
58
 
59
59
  AllCops:
60
60
  Exclude:
61
- - bin/**/*
62
- - Rakefile
63
- - config/**/*
64
- - test/**/*
61
+ - vendor/bundle/**/*
data/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
- # v0.4.0 (02.05.2021)
1
+ # v0.6.0 (21.01.2023)
2
+
3
+ - Support verification of HS256-signed JWTs (https://github.com/omniauth/omniauth_openid_connect/pull/134)
4
+
5
+ # v0.5.0 (26.12.2022)
6
+
7
+ - Support the "nonce" parameter forwarding without a session [#130](https://github.com/omniauth/omniauth_openid_connect/pull/130)
8
+ - Fetch key from JWKS URI if available [#133](https://github.com/omniauth/omniauth_openid_connect/pull/133)
9
+ - Make the state parameter verification optional [#122](https://github.com/omniauth/omniauth_openid_connect/pull/122)
10
+ - Add email_verified claim in user info [#131](https://github.com/omniauth/omniauth_openid_connect/pull/131)
11
+ - Add PKCE verification support [#128](https://github.com/omniauth/omniauth_openid_connect/pull/128)
12
+
13
+ # v0.4.0 (06.02.2022)
2
14
 
3
15
  - Support dynamic parameters to the authorize URI [#90](https://github.com/omniauth/omniauth_openid_connect/pull/90)
4
16
  - Upgrade Faker and replace Travis with Github Actions [#102](https://github.com/omniauth/omniauth_openid_connect/pull/102)
data/Gemfile CHANGED
@@ -2,3 +2,9 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
  gemspec
5
+
6
+ if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('3.1')
7
+ gem 'net-imap', require: false
8
+ gem 'net-pop', require: false
9
+ gem 'net-smtp', require: false
10
+ end
data/README.md CHANGED
@@ -1,12 +1,3 @@
1
- # Maintainers Wanted
2
-
3
- This project is looking for maintainers.
4
-
5
- Due to lack of using this gem in my projects I have no time to keep it alive.
6
- Feel free to open an issue if you interested.
7
-
8
- _This project is built and maintained 100% voluntarily._
9
-
10
1
  # OmniAuth::OpenIDConnect
11
2
 
12
3
  Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-openid-connect)
@@ -14,6 +5,7 @@ Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-open
14
5
  I've forked this repository and launch as separate gem because maintaining of original was dropped.
15
6
 
16
7
  [![Build Status](https://github.com/omniauth/omniauth_openid_connect/actions/workflows/main.yml/badge.svg)](https://github.com/omniauth/omniauth_openid_connect/actions/workflows/main.yml)
8
+ [![Coverage Status](https://coveralls.io/repos/github/omniauth/omniauth_openid_connect/badge.svg)](https://coveralls.io/github/omniauth/omniauth_openid_connect)
17
9
 
18
10
  ## Installation
19
11
 
@@ -31,7 +23,7 @@ Or install it yourself as:
31
23
 
32
24
  ## Supported Ruby Versions
33
25
 
34
- OmniAuth::OpenIDConnect is tested under 2.4, 2.5, 2.6, 2.7
26
+ OmniAuth::OpenIDConnect is tested under 2.5, 2.6, 2.7, 3.0, 3.1
35
27
 
36
28
  ## Usage
37
29
 
@@ -55,24 +47,29 @@ config.omniauth :openid_connect, {
55
47
 
56
48
  ### Options Overview
57
49
 
58
- | Field | Description | Required | Default | Example/Options |
59
- |------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------|-----------------------------------------------------|
60
- | name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
61
- | issuer | Root url for the authorization server | yes | | https://myprovider.com |
62
- | discovery | Should OpenID discovery be used. This is recommended if the IDP provides a discovery endpoint. See client config for how to manually enter discovered values. | no | false | one of: true, false |
63
- | client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
64
- | scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
65
- | response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
66
- | state | A value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string. | no | Random 16 character string | Proc.new { SecureRandom.hex(32) } |
67
- | response_mode | The response mode per [spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | nil | one of: :query, :fragment, :form_post, :web_message |
68
- | display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
69
- | prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |
70
- | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
71
- | post_logout_redirect_uri | The logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | empty | https://myapp.com/logout/callback |
72
- | uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
73
- | extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} |
74
- | allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] |
75
- | client_options | A hash of client options detailed in its own section | yes | | |
50
+ | Field | Description | Required | Default | Example/Options |
51
+ |------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------|-----------------------------------------------------|
52
+ | name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
53
+ | issuer | Root url for the authorization server | yes | | https://myprovider.com |
54
+ | discovery | Should OpenID discovery be used. This is recommended if the IDP provides a discovery endpoint. See client config for how to manually enter discovered values. | no | false | one of: true, false |
55
+ | client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
56
+ | scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
57
+ | response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
58
+ | state | A value to be used for the OAuth2 state parameter on the authorization request. Can be a proc that generates a string. | no | Random 16 character string | Proc.new { SecureRandom.hex(32) } |
59
+ | require_state | Should state param be verified - this is recommended, not required by the OIDC specification | no | true | false |
60
+ | response_mode | The response mode per [spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html) | no | nil | one of: :query, :fragment, :form_post, :web_message |
61
+ | display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
62
+ | prompt | An optional parameter to the authrization request to determine what pages the user will be shown | no | nil | one of: :none, :login, :consent, :select_account |
63
+ | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
64
+ | post_logout_redirect_uri | The logout redirect uri to use per the [session management draft](https://openid.net/specs/openid-connect-session-1_0.html) | no | empty | https://myapp.com/logout/callback |
65
+ | uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
66
+ | extra_authorize_params | A hash of extra fixed parameters that will be merged to the authorization request | no | Hash | {"tenant" => "common"} |
67
+ | allow_authorize_params | A list of allowed dynamic parameters that will be merged to the authorization request | no | Array | [:screen_name] |
68
+ | pkce | Enable [PKCE flow](https://oauth.net/2/pkce/) | no | false | one of: true, false |
69
+ | pkce_verifier | Specify a custom PKCE verifier code. | no | A random 128-char string | Proc.new { SecureRandom.hex(64) } |
70
+ | pkce_options | Specify a custom implementation of the PKCE code challenge/method. | no | SHA256(code_challenge) in hex | Proc to customise the code challenge generation |
71
+ | client_options | A hash of client options detailed in its own section | yes | | |
72
+ | jwt_secret_base64 | For HMAC with SHA2 (e.g. HS256) signing algorithms, specify the base64-encoded secret used to sign the JWT token. Defaults to the OAuth2 client secret if not specified. | no | client_options.secret | "bXlzZWNyZXQ=\n"
76
73
 
77
74
  ### Client Config Options
78
75
 
@@ -102,7 +99,7 @@ These are the configuration options for the client_options hash of the configura
102
99
 
103
100
  * `response_type` tells the authorization server which grant type the application wants to use,
104
101
  currently, only `:code` (Authorization Code grant) and `:id_token` (Implicit grant) are valid.
105
- * If you want to pass `state` paramete by yourself. You can set Proc Object.
102
+ * If you want to pass `state` parameter by yourself. You can set Proc Object.
106
103
  e.g. `state: Proc.new { SecureRandom.hex(32) }`
107
104
  * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify
108
105
  `false` to `send_nonce` option. (default true)
@@ -124,6 +121,11 @@ These are the configuration options for the client_options hash of the configura
124
121
  property can be used to add the attribute to the token request. Initial value is `true`, which means that the
125
122
  scope attribute is included by default.
126
123
 
124
+ ## Additional notes
125
+ * In some cases, you may want to go straight to the callback phase - e.g. when requested by a stateless client, like a mobile app.
126
+ In such example, the session is empty, so you have to forward certain parameters received from the client.
127
+ Currently supported ones are `code_verifier` and `nonce` - simply provide them as the `/callback` request parameters.
128
+
127
129
  For the full low down on OpenID Connect, please check out
128
130
  [the spec](http://openid.net/specs/openid-connect-core-1_0.html).
129
131
 
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
- VERSION = '0.4.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'addressable/uri'
3
+ require 'base64'
4
4
  require 'timeout'
5
5
  require 'net/http'
6
6
  require 'open-uri'
@@ -10,7 +10,7 @@ require 'forwardable'
10
10
 
11
11
  module OmniAuth
12
12
  module Strategies
13
- class OpenIDConnect
13
+ class OpenIDConnect # rubocop:disable Metrics/ClassLength
14
14
  include OmniAuth::Strategy
15
15
  extend Forwardable
16
16
 
@@ -37,10 +37,12 @@ module OmniAuth
37
37
  option :issuer
38
38
  option :discovery, false
39
39
  option :client_signing_alg
40
+ option :jwt_secret_base64
40
41
  option :client_jwk_signing_key
41
42
  option :client_x509_signing_key
42
43
  option :scope, [:openid]
43
44
  option :response_type, 'code' # ['code', 'id_token']
45
+ option :require_state, true
44
46
  option :state
45
47
  option :response_mode # [:query, :fragment, :form_post, :web_message]
46
48
  option :display, nil # [:page, :popup, :touch, :wap]
@@ -57,6 +59,14 @@ module OmniAuth
57
59
  option :extra_authorize_params, {}
58
60
  option :allow_authorize_params, []
59
61
  option :uid_field, 'sub'
62
+ option :pkce, false
63
+ option :pkce_verifier, nil
64
+ option :pkce_options, {
65
+ code_challenge: proc { |verifier|
66
+ Base64.urlsafe_encode64(Digest::SHA2.digest(verifier), padding: false)
67
+ },
68
+ code_challenge_method: 'S256',
69
+ }
60
70
 
61
71
  def uid
62
72
  user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
@@ -66,6 +76,7 @@ module OmniAuth
66
76
  {
67
77
  name: user_info.name,
68
78
  email: user_info.email,
79
+ email_verified: user_info.email_verified,
69
80
  nickname: user_info.preferred_username,
70
81
  first_name: user_info.given_name,
71
82
  last_name: user_info.family_name,
@@ -107,7 +118,7 @@ module OmniAuth
107
118
  def callback_phase
108
119
  error = params['error_reason'] || params['error']
109
120
  error_description = params['error_description'] || params['error_reason']
110
- invalid_state = params['state'].to_s.empty? || params['state'] != stored_state
121
+ invalid_state = (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
111
122
 
112
123
  raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
113
124
  raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state
@@ -178,17 +189,52 @@ module OmniAuth
178
189
  opts[key] = request.params[key.to_s] unless opts.key?(key)
179
190
  end
180
191
 
192
+ if options.pkce
193
+ verifier = options.pkce_verifier ? options.pkce_verifier.call : SecureRandom.hex(64)
194
+
195
+ opts.merge!(pkce_authorize_params(verifier))
196
+ session['omniauth.pkce.verifier'] = verifier
197
+ end
198
+
181
199
  client.authorization_uri(opts.reject { |_k, v| v.nil? })
182
200
  end
183
201
 
184
202
  def public_key
185
- return config.jwks if options.discovery
203
+ @public_key ||= if options.discovery
204
+ config.jwks
205
+ elsif configured_public_key
206
+ configured_public_key
207
+ elsif client_options.jwks_uri
208
+ fetch_key
209
+ end
210
+ end
211
+
212
+ # Some OpenID providers use the OAuth2 client secret as the shared secret, but
213
+ # Keycloak uses a separate key that's stored inside the database.
214
+ def secret
215
+ base64_decoded_jwt_secret || client_options.secret
216
+ end
186
217
 
187
- key_or_secret || config.jwks
218
+ def pkce_authorize_params(verifier)
219
+ # NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
220
+ {
221
+ code_challenge: options.pkce_options[:code_challenge].call(verifier),
222
+ code_challenge_method: options.pkce_options[:code_challenge_method],
223
+ }
188
224
  end
189
225
 
190
226
  private
191
227
 
228
+ def fetch_key
229
+ @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get_content(client_options.jwks_uri))
230
+ end
231
+
232
+ def base64_decoded_jwt_secret
233
+ return unless options.jwt_secret_base64
234
+
235
+ Base64.decode64(options.jwt_secret_base64)
236
+ end
237
+
192
238
  def issuer
193
239
  resource = "#{ client_options.scheme }://#{ client_options.host }"
194
240
  resource = "#{ resource }:#{ client_options.port }" if client_options.port
@@ -220,18 +266,88 @@ module OmniAuth
220
266
  def access_token
221
267
  return @access_token if @access_token
222
268
 
223
- @access_token = client.access_token!(
269
+ token_request_params = {
224
270
  scope: (options.scope if options.send_scope_to_token_endpoint),
225
- client_auth_method: options.client_auth_method
226
- )
271
+ client_auth_method: options.client_auth_method,
272
+ }
227
273
 
274
+ token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce
275
+
276
+ @access_token = client.access_token!(token_request_params)
228
277
  verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
229
278
 
230
279
  @access_token
231
280
  end
232
281
 
282
+ # Unlike ::OpenIDConnect::ResponseObject::IdToken.decode, this
283
+ # method splits the decoding and verification of JWT into two
284
+ # steps. First, we decode the JWT without verifying it to
285
+ # determine the algorithm used to sign. Then, we verify it using
286
+ # the appropriate public key (e.g. if algorithm is RS256) or
287
+ # shared secret (e.g. if algorithm is HS256). This works around a
288
+ # limitation in the openid_connect gem:
289
+ # https://github.com/nov/openid_connect/issues/61
233
290
  def decode_id_token(id_token)
234
- ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, public_key)
291
+ decoded = JSON::JWT.decode(id_token, :skip_verification)
292
+ algorithm = decoded.algorithm.to_sym
293
+
294
+ validate_client_algorithm!(algorithm)
295
+
296
+ keyset =
297
+ case algorithm
298
+ when :HS256, :HS384, :HS512
299
+ secret
300
+ else
301
+ public_key
302
+ end
303
+
304
+ decoded.verify!(keyset)
305
+ ::OpenIDConnect::ResponseObject::IdToken.new(decoded)
306
+ rescue JSON::JWK::Set::KidNotFound
307
+ # If the JWT has a key ID (kid), then we know that the set of
308
+ # keys supplied doesn't contain the one we want, and we're
309
+ # done. However, if there is no kid, then we try each key
310
+ # individually to see if one works:
311
+ # https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
312
+ raise if decoded&.header&.key?('kid')
313
+
314
+ decoded = decode_with_each_key!(id_token, keyset)
315
+
316
+ raise unless decoded
317
+
318
+ decoded
319
+ end
320
+
321
+ # If client_signing_alg is specified, we check that the returned JWT
322
+ # matches the expected algorithm. If not, we reject it.
323
+ def validate_client_algorithm!(algorithm)
324
+ client_signing_alg = options.client_signing_alg&.to_sym
325
+
326
+ return unless client_signing_alg
327
+ return if algorithm == client_signing_alg
328
+
329
+ reason = "Received JWT is signed with #{algorithm}, but client_singing_alg is configured for #{client_signing_alg}"
330
+ raise CallbackError, error: :invalid_jwt_algorithm, reason: reason, uri: params['error_uri']
331
+ end
332
+
333
+ def decode!(id_token, key)
334
+ ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
335
+ end
336
+
337
+ def decode_with_each_key!(id_token, keyset)
338
+ return unless keyset.is_a?(JSON::JWK::Set)
339
+
340
+ keyset.each do |key|
341
+ begin
342
+ decoded = decode!(id_token, key)
343
+ rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWS::UnknownAlgorithm
344
+ next
345
+ end
346
+
347
+ return decoded if decoded
348
+ end
349
+
350
+ nil
235
351
  end
236
352
 
237
353
  def client_options
@@ -273,17 +389,12 @@ module OmniAuth
273
389
  super
274
390
  end
275
391
 
276
- def key_or_secret
277
- case options.client_signing_alg
278
- when :HS256, :HS384, :HS512
279
- client_options.secret
280
- when :RS256, :RS384, :RS512
281
- if options.client_jwk_signing_key
282
- parse_jwk_key(options.client_jwk_signing_key)
283
- elsif options.client_x509_signing_key
284
- parse_x509_key(options.client_x509_signing_key)
285
- end
286
- end
392
+ def configured_public_key
393
+ @configured_public_key ||= if options.client_jwk_signing_key
394
+ parse_jwk_key(options.client_jwk_signing_key)
395
+ elsif options.client_x509_signing_key
396
+ parse_x509_key(options.client_x509_signing_key)
397
+ end
287
398
  end
288
399
 
289
400
  def parse_x509_key(key)
@@ -353,7 +464,7 @@ module OmniAuth
353
464
 
354
465
  decode_id_token(id_token).verify!(issuer: options.issuer,
355
466
  client_id: client_options.identifier,
356
- nonce: stored_nonce)
467
+ nonce: params['nonce'].presence || stored_nonce)
357
468
  end
358
469
 
359
470
  class CallbackError < StandardError
@@ -27,10 +27,8 @@ Gem::Specification.new do |spec|
27
27
  'rubygems_mfa_required' => 'true',
28
28
  }
29
29
 
30
- spec.add_dependency 'addressable', '~> 2.5'
31
30
  spec.add_dependency 'omniauth', '>= 1.9', '< 3'
32
31
  spec.add_dependency 'openid_connect', '~> 1.1'
33
- spec.add_development_dependency 'coveralls', '~> 0.8'
34
32
  spec.add_development_dependency 'faker', '~> 2.0'
35
33
  spec.add_development_dependency 'guard', '~> 2.14'
36
34
  spec.add_development_dependency 'guard-bundler', '~> 2.2'
@@ -39,5 +37,6 @@ Gem::Specification.new do |spec|
39
37
  spec.add_development_dependency 'mocha', '~> 1.7'
40
38
  spec.add_development_dependency 'rake', '~> 12.0'
41
39
  spec.add_development_dependency 'rubocop', '~> 1.12'
42
- spec.add_development_dependency 'simplecov', '~> 0.12'
40
+ spec.add_development_dependency 'simplecov', '~> 0.21'
41
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8'
43
42
  end