omniauth_openid_connect 0.3.5 → 0.5.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: 9c030571ea9bcbd861ebe4d8455282c0b1b34c17af77294660b1c0123ed976ab
4
- data.tar.gz: 2f4b2a8cd026797260f36e358ac7678a6827486889f23aab325d08e0d390b649
3
+ metadata.gz: 34218d7218e2aca766623a1252b641baa20d81330cfd38c67a733f9221d76aad
4
+ data.tar.gz: 54f874832122f3cc1444280d8820d222a4dff7ef4c3580eb899d4da1efb38051
5
5
  SHA512:
6
- metadata.gz: afdf6363a18b019939a88abc612be51b20780fdeae2f1f300ef5e9df5a1002a97812b17360939e1ba9bb0b3bf4e54471e3da0ec6de858ac2afa40df15fdfb23d
7
- data.tar.gz: e449bd59e5e75cfcce12ae27e5072fb8e3fe821e969a510856bbad0ed2553252345826ed16e97ecdbe1405a8c4be275195f759e71aed99fd699bf9509bffa0f3
6
+ metadata.gz: e4465213a06e82fd61d9997d0ec1b9f993620dead092add6ba2f07b34aa08dca53bcb63d92a256839c7310478c89ecff9359778e5721cf1cc6dcc300e5d780f8
7
+ data.tar.gz: 8fdbda1f579f271f6273a6380a7a9cf581b4b1239b589a1e2cb98799b8731056c3ec222c519e445d95387749ab62b58839e4005906938e0e1f76b9d396e76565
@@ -0,0 +1,63 @@
1
+ name: Main
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ - master
7
+
8
+ pull_request:
9
+ types: [opened, synchronize, reopened]
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ ruby: ["2.5", "2.6", "2.7", "3.0", "3.1"]
18
+ name: Ruby ${{ matrix.ruby }}
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v2
23
+
24
+ - name: Setup Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: ${{ matrix.ruby }}
28
+ bundler-cache: true
29
+
30
+ - name: Run tests
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
@@ -1,3 +1,6 @@
1
+ Gemspec/RequiredRubyVersion:
2
+ Enabled: false
3
+
1
4
  LineLength:
2
5
  Description: 'Limit lines to 130 characters.'
3
6
  Max: 130
@@ -36,7 +39,10 @@ Documentation:
36
39
  Enabled: false
37
40
 
38
41
  Metrics/AbcSize:
39
- Max: 50
42
+ Max: 60
43
+
44
+ Metrics/ClassLength:
45
+ Max: 300
40
46
 
41
47
  Metrics/CyclomaticComplexity:
42
48
  Max: 50
@@ -52,7 +58,4 @@ Metrics/MethodLength:
52
58
 
53
59
  AllCops:
54
60
  Exclude:
55
- - bin/**/*
56
- - Rakefile
57
- - config/**/*
58
- - test/**/*
61
+ - vendor/bundle/**/*
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # v0.5.0 (26.12.2022)
2
+
3
+ - Support the "nonce" parameter forwarding without a session [#130](https://github.com/omniauth/omniauth_openid_connect/pull/130)
4
+ - Fetch key from JWKS URI if available [#133](https://github.com/omniauth/omniauth_openid_connect/pull/133)
5
+ - Make the state parameter verification optional [#122](https://github.com/omniauth/omniauth_openid_connect/pull/122)
6
+ - Add email_verified claim in user info [#131](https://github.com/omniauth/omniauth_openid_connect/pull/131)
7
+ - Add PKCE verification support [#128](https://github.com/omniauth/omniauth_openid_connect/pull/128)
8
+
9
+ # v0.4.0 (06.02.2022)
10
+
11
+ - Support dynamic parameters to the authorize URI [#90](https://github.com/omniauth/omniauth_openid_connect/pull/90)
12
+ - Upgrade Faker and replace Travis with Github Actions [#102](https://github.com/omniauth/omniauth_openid_connect/pull/102)
13
+ - Make `omniauth_openid_connect` gem compatible with `omniauth v2.0` [#95](https://github.com/omniauth/omniauth_openid_connect/pull/95)
14
+ - Fall back to the discovered jwks when no key specified [#97](https://github.com/omniauth/omniauth_openid_connect/pull/97)
15
+ - Allow updating to omniauth v2 [#88](https://github.com/omniauth/omniauth_openid_connect/pull/88)
16
+
1
17
  # v0.3.5 (07.06.2020)
2
18
 
3
19
  - bugfix: Info from decoded id_token is not exposed into `request.env['omniauth.auth']` [#61](https://github.com/m0n9oose/omniauth_openid_connect/pull/61)
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/Guardfile CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  guard 'minitest' do
7
7
  # with Minitest::Unit
8
- watch(%r{^test/(.*)\/(.*)_test\.rb})
8
+ watch(%r{^test/(.*)/(.*)_test\.rb})
9
9
  watch(%r{^lib/(.*)\.rb}) { |m| "test/lib/#{m[1]}_test.rb" }
10
10
  watch(%r{^test/test_helper\.rb}) { 'test' }
11
11
  end
data/README.md CHANGED
@@ -4,7 +4,8 @@ Originally was [omniauth-openid-connect](https://github.com/jjbohn/omniauth-open
4
4
 
5
5
  I've forked this repository and launch as separate gem because maintaining of original was dropped.
6
6
 
7
- [![Build Status](https://travis-ci.org/m0n9oose/omniauth_openid_connect.png?branch=master)](https://travis-ci.org/m0n9oose/omniauth_openid_connect)
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)
8
9
 
9
10
  ## Installation
10
11
 
@@ -19,10 +20,10 @@ And then execute:
19
20
  Or install it yourself as:
20
21
 
21
22
  $ gem install omniauth_openid_connect
22
-
23
+
23
24
  ## Supported Ruby Versions
24
25
 
25
- 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
26
27
 
27
28
  ## Usage
28
29
 
@@ -46,22 +47,28 @@ config.omniauth :openid_connect, {
46
47
 
47
48
  ### Options Overview
48
49
 
49
- | Field | Description | Required | Default | Example/Options |
50
- |------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|----------------------------|-----------------------------------------------------|
51
- | name | Arbitrary string to identify connection and identify it from other openid_connect providers | no | String: openid_connect | :my_idp |
52
- | issuer | Root url for the authorization server | yes | | https://myprovider.com |
53
- | 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 |
54
- | client_auth_method | Which authentication method to use to authenticate your app with the authorization server | no | Sym: basic | "basic", "jwks" |
55
- | scope | Which OpenID scopes to include (:openid is always required) | no | Array<sym> [:openid] | [:openid, :profile, :email] |
56
- | response_type | Which OAuth2 response type to use with the authorization request | no | String: code | one of: 'code', 'id_token' |
57
- | 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) } |
58
- | 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 |
59
- | display | An optional parameter to the authorization request to determine how the authorization and consent page | no | nil | one of: :page, :popup, :touch, :wap |
60
- | 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 |
61
- | send_scope_to_token_endpoint | Should the scope parameter be sent to the authorization token endpoint? | no | true | one of: true, false |
62
- | 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 |
63
- | uid_field | The field of the user info response to be used as a unique id | no | 'sub' | "sub", "preferred_username" |
64
- | 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 | | |
65
72
 
66
73
  ### Client Config Options
67
74
 
@@ -91,7 +98,7 @@ These are the configuration options for the client_options hash of the configura
91
98
 
92
99
  * `response_type` tells the authorization server which grant type the application wants to use,
93
100
  currently, only `:code` (Authorization Code grant) and `:id_token` (Implicit grant) are valid.
94
- * If you want to pass `state` paramete by yourself. You can set Proc Object.
101
+ * If you want to pass `state` parameter by yourself. You can set Proc Object.
95
102
  e.g. `state: Proc.new { SecureRandom.hex(32) }`
96
103
  * `nonce` is optional. If don't want to pass "nonce" parameter to provider, You should specify
97
104
  `false` to `send_nonce` option. (default true)
@@ -113,6 +120,11 @@ These are the configuration options for the client_options hash of the configura
113
120
  property can be used to add the attribute to the token request. Initial value is `true`, which means that the
114
121
  scope attribute is included by default.
115
122
 
123
+ ## Additional notes
124
+ * 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.
125
+ In such example, the session is empty, so you have to forward certain parameters received from the client.
126
+ Currently supported ones are `code_verifier` and `nonce` - simply provide them as the `/callback` request parameters.
127
+
116
128
  For the full low down on OpenID Connect, please check out
117
129
  [the spec](http://openid.net/specs/openid-connect-core-1_0.html).
118
130
 
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
 
@@ -3,7 +3,9 @@
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
5
  class Error < RuntimeError; end
6
+
6
7
  class MissingCodeError < Error; end
8
+
7
9
  class MissingIdTokenError < Error; end
8
10
  end
9
11
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OmniAuth
4
4
  module OpenIDConnect
5
- VERSION = '0.3.5'
5
+ VERSION = '0.5.0'
6
6
  end
7
7
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'addressable/uri'
4
3
  require 'timeout'
5
4
  require 'net/http'
6
5
  require 'open-uri'
@@ -10,7 +9,7 @@ require 'forwardable'
10
9
 
11
10
  module OmniAuth
12
11
  module Strategies
13
- class OpenIDConnect
12
+ class OpenIDConnect # rubocop:disable Metrics/ClassLength
14
13
  include OmniAuth::Strategy
15
14
  extend Forwardable
16
15
 
@@ -41,6 +40,7 @@ module OmniAuth
41
40
  option :client_x509_signing_key
42
41
  option :scope, [:openid]
43
42
  option :response_type, 'code' # ['code', 'id_token']
43
+ option :require_state, true
44
44
  option :state
45
45
  option :response_mode # [:query, :fragment, :form_post, :web_message]
46
46
  option :display, nil # [:page, :popup, :touch, :wap]
@@ -55,7 +55,16 @@ module OmniAuth
55
55
  option :client_auth_method
56
56
  option :post_logout_redirect_uri
57
57
  option :extra_authorize_params, {}
58
+ option :allow_authorize_params, []
58
59
  option :uid_field, 'sub'
60
+ option :pkce, false
61
+ option :pkce_verifier, nil
62
+ option :pkce_options, {
63
+ code_challenge: proc { |verifier|
64
+ Base64.urlsafe_encode64(Digest::SHA2.digest(verifier), padding: false)
65
+ },
66
+ code_challenge_method: 'S256',
67
+ }
59
68
 
60
69
  def uid
61
70
  user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
@@ -65,6 +74,7 @@ module OmniAuth
65
74
  {
66
75
  name: user_info.name,
67
76
  email: user_info.email,
77
+ email_verified: user_info.email_verified,
68
78
  nickname: user_info.preferred_username,
69
79
  first_name: user_info.given_name,
70
80
  last_name: user_info.family_name,
@@ -106,7 +116,7 @@ module OmniAuth
106
116
  def callback_phase
107
117
  error = params['error_reason'] || params['error']
108
118
  error_description = params['error_description'] || params['error_reason']
109
- invalid_state = params['state'].to_s.empty? || params['state'] != stored_state
119
+ invalid_state = (options.require_state && params['state'].to_s.empty?) || params['state'] != stored_state
110
120
 
111
121
  raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
112
122
  raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state
@@ -173,17 +183,44 @@ module OmniAuth
173
183
 
174
184
  opts.merge!(options.extra_authorize_params) unless options.extra_authorize_params.empty?
175
185
 
186
+ options.allow_authorize_params.each do |key|
187
+ opts[key] = request.params[key.to_s] unless opts.key?(key)
188
+ end
189
+
190
+ if options.pkce
191
+ verifier = options.pkce_verifier ? options.pkce_verifier.call : SecureRandom.hex(64)
192
+
193
+ opts.merge!(pkce_authorize_params(verifier))
194
+ session['omniauth.pkce.verifier'] = verifier
195
+ end
196
+
176
197
  client.authorization_uri(opts.reject { |_k, v| v.nil? })
177
198
  end
178
199
 
179
200
  def public_key
180
- return config.jwks if options.discovery
201
+ @public_key ||= if options.discovery
202
+ config.jwks
203
+ elsif key_or_secret
204
+ key_or_secret
205
+ elsif client_options.jwks_uri
206
+ fetch_key
207
+ end
208
+ end
181
209
 
182
- key_or_secret
210
+ def pkce_authorize_params(verifier)
211
+ # NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
212
+ {
213
+ code_challenge: options.pkce_options[:code_challenge].call(verifier),
214
+ code_challenge_method: options.pkce_options[:code_challenge_method],
215
+ }
183
216
  end
184
217
 
185
218
  private
186
219
 
220
+ def fetch_key
221
+ @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get_content(client_options.jwks_uri))
222
+ end
223
+
187
224
  def issuer
188
225
  resource = "#{ client_options.scheme }://#{ client_options.host }"
189
226
  resource = "#{ resource }:#{ client_options.port }" if client_options.port
@@ -215,11 +252,14 @@ module OmniAuth
215
252
  def access_token
216
253
  return @access_token if @access_token
217
254
 
218
- @access_token = client.access_token!(
255
+ token_request_params = {
219
256
  scope: (options.scope if options.send_scope_to_token_endpoint),
220
- client_auth_method: options.client_auth_method
221
- )
257
+ client_auth_method: options.client_auth_method,
258
+ }
222
259
 
260
+ token_request_params[:code_verifier] = params['code_verifier'] || session.delete('omniauth.pkce.verifier') if options.pkce
261
+
262
+ @access_token = client.access_token!(token_request_params)
223
263
  verify_id_token!(@access_token.id_token) if configured_response_type == 'code'
224
264
 
225
265
  @access_token
@@ -256,6 +296,12 @@ module OmniAuth
256
296
  session.delete('omniauth.nonce')
257
297
  end
258
298
 
299
+ def script_name
300
+ return '' if @env.nil?
301
+
302
+ super
303
+ end
304
+
259
305
  def session
260
306
  return {} if @env.nil?
261
307
 
@@ -263,16 +309,17 @@ module OmniAuth
263
309
  end
264
310
 
265
311
  def key_or_secret
266
- case options.client_signing_alg
267
- when :HS256, :HS384, :HS512
268
- client_options.secret
269
- when :RS256, :RS384, :RS512
270
- if options.client_jwk_signing_key
271
- parse_jwk_key(options.client_jwk_signing_key)
272
- elsif options.client_x509_signing_key
273
- parse_x509_key(options.client_x509_signing_key)
312
+ @key_or_secret ||=
313
+ case options.client_signing_alg&.to_sym
314
+ when :HS256, :HS384, :HS512
315
+ client_options.secret
316
+ when :RS256, :RS384, :RS512
317
+ if options.client_jwk_signing_key
318
+ parse_jwk_key(options.client_jwk_signing_key)
319
+ elsif options.client_x509_signing_key
320
+ parse_x509_key(options.client_x509_signing_key)
321
+ end
274
322
  end
275
- end
276
323
  end
277
324
 
278
325
  def parse_x509_key(key)
@@ -342,13 +389,14 @@ module OmniAuth
342
389
 
343
390
  decode_id_token(id_token).verify!(issuer: options.issuer,
344
391
  client_id: client_options.identifier,
345
- nonce: stored_nonce)
392
+ nonce: params['nonce'].presence || stored_nonce)
346
393
  end
347
394
 
348
395
  class CallbackError < StandardError
349
396
  attr_accessor :error, :error_reason, :error_uri
350
397
 
351
398
  def initialize(data)
399
+ super
352
400
  self.error = data[:error]
353
401
  self.error_reason = data[:reason]
354
402
  self.error_uri = data[:uri]
@@ -19,17 +19,24 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ['lib']
21
21
 
22
- spec.add_dependency 'addressable', '~> 2.5'
23
- spec.add_dependency 'omniauth', '~> 1.9'
22
+ spec.metadata = {
23
+ 'bug_tracker_uri' => 'https://github.com/m0n9oose/omniauth_openid_connect/issues',
24
+ 'changelog_uri' => 'https://github.com/m0n9oose/omniauth_openid_connect/releases',
25
+ 'documentation_uri' => "https://github.com/m0n9oose/omniauth_openid_connect/tree/v#{spec.version}#readme",
26
+ 'source_code_uri' => "https://github.com/m0n9oose/omniauth_openid_connect/tree/v#{spec.version}",
27
+ 'rubygems_mfa_required' => 'true',
28
+ }
29
+
30
+ spec.add_dependency 'omniauth', '>= 1.9', '< 3'
24
31
  spec.add_dependency 'openid_connect', '~> 1.1'
25
- spec.add_development_dependency 'coveralls', '~> 0.8'
26
- spec.add_development_dependency 'faker', '~> 1.6'
32
+ spec.add_development_dependency 'faker', '~> 2.0'
27
33
  spec.add_development_dependency 'guard', '~> 2.14'
28
34
  spec.add_development_dependency 'guard-bundler', '~> 2.2'
29
35
  spec.add_development_dependency 'guard-minitest', '~> 2.4'
30
36
  spec.add_development_dependency 'minitest', '~> 5.1'
31
37
  spec.add_development_dependency 'mocha', '~> 1.7'
32
38
  spec.add_development_dependency 'rake', '~> 12.0'
33
- spec.add_development_dependency 'rubocop', '~> 0.63'
34
- spec.add_development_dependency 'simplecov', '~> 0.12'
39
+ spec.add_development_dependency 'rubocop', '~> 1.12'
40
+ spec.add_development_dependency 'simplecov', '~> 0.21'
41
+ spec.add_development_dependency 'simplecov-lcov', '~> 0.8'
35
42
  end