omniauth-azure-activedirectory 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rubocop.yml +8 -0
  4. data/.rubocop_todo.yml +20 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +98 -0
  9. data/Rakefile +22 -0
  10. data/examples/rails-todo-list-app/.gitignore +25 -0
  11. data/examples/rails-todo-list-app/Gemfile +33 -0
  12. data/examples/rails-todo-list-app/README.md +83 -0
  13. data/examples/rails-todo-list-app/Rakefile +3 -0
  14. data/examples/rails-todo-list-app/app/assets/javascripts/application.js +4 -0
  15. data/examples/rails-todo-list-app/app/assets/stylesheets/application.css +2 -0
  16. data/examples/rails-todo-list-app/app/controllers/application_controller.rb +3 -0
  17. data/examples/rails-todo-list-app/app/controllers/home_controller.rb +2 -0
  18. data/examples/rails-todo-list-app/app/controllers/profile_controller.rb +20 -0
  19. data/examples/rails-todo-list-app/app/controllers/sessions_controller.rb +28 -0
  20. data/examples/rails-todo-list-app/app/controllers/signed_in_controller.rb +25 -0
  21. data/examples/rails-todo-list-app/app/controllers/tasks_controller.rb +33 -0
  22. data/examples/rails-todo-list-app/app/models/task.rb +10 -0
  23. data/examples/rails-todo-list-app/app/models/user.rb +58 -0
  24. data/examples/rails-todo-list-app/app/views/home/index.html.haml +4 -0
  25. data/examples/rails-todo-list-app/app/views/layouts/application.html.haml +12 -0
  26. data/examples/rails-todo-list-app/app/views/layouts/signed_in.html.haml +18 -0
  27. data/examples/rails-todo-list-app/app/views/profile/index.html.haml +13 -0
  28. data/examples/rails-todo-list-app/app/views/tasks/index.html.haml +11 -0
  29. data/examples/rails-todo-list-app/bin/bundle +3 -0
  30. data/examples/rails-todo-list-app/bin/rails +4 -0
  31. data/examples/rails-todo-list-app/bin/rake +4 -0
  32. data/examples/rails-todo-list-app/bin/setup +29 -0
  33. data/examples/rails-todo-list-app/config.ru +4 -0
  34. data/examples/rails-todo-list-app/config/application.rb +29 -0
  35. data/examples/rails-todo-list-app/config/boot.rb +3 -0
  36. data/examples/rails-todo-list-app/config/database.yml +25 -0
  37. data/examples/rails-todo-list-app/config/environment.rb +13 -0
  38. data/examples/rails-todo-list-app/config/environments/development.rb +41 -0
  39. data/examples/rails-todo-list-app/config/environments/production.rb +79 -0
  40. data/examples/rails-todo-list-app/config/environments/test.rb +42 -0
  41. data/examples/rails-todo-list-app/config/initializers/assets.rb +11 -0
  42. data/examples/rails-todo-list-app/config/initializers/backtrace_silencers.rb +7 -0
  43. data/examples/rails-todo-list-app/config/initializers/cookies_serializer.rb +3 -0
  44. data/examples/rails-todo-list-app/config/initializers/filter_parameter_logging.rb +4 -0
  45. data/examples/rails-todo-list-app/config/initializers/inflections.rb +16 -0
  46. data/examples/rails-todo-list-app/config/initializers/mime_types.rb +4 -0
  47. data/examples/rails-todo-list-app/config/initializers/omniauth.rb +3 -0
  48. data/examples/rails-todo-list-app/config/initializers/session_store.rb +3 -0
  49. data/examples/rails-todo-list-app/config/initializers/wrap_parameters.rb +14 -0
  50. data/examples/rails-todo-list-app/config/routes.rb +22 -0
  51. data/examples/rails-todo-list-app/db/schema.rb +35 -0
  52. data/examples/rails-todo-list-app/public/404.html +67 -0
  53. data/examples/rails-todo-list-app/public/422.html +67 -0
  54. data/examples/rails-todo-list-app/public/500.html +66 -0
  55. data/examples/rails-todo-list-app/public/favicon.ico +0 -0
  56. data/examples/sinatra-multiple-providers-app/.env +11 -0
  57. data/examples/sinatra-multiple-providers-app/Gemfile +8 -0
  58. data/examples/sinatra-multiple-providers-app/README.md +13 -0
  59. data/examples/sinatra-multiple-providers-app/app.rb +51 -0
  60. data/examples/sinatra-multiple-providers-app/config.ru +45 -0
  61. data/lib/omniauth-azure-activedirectory.rb +23 -0
  62. data/lib/omniauth/azure_activedirectory.rb +24 -0
  63. data/lib/omniauth/azure_activedirectory/version.rb +28 -0
  64. data/lib/omniauth/strategies/azure_activedirectory.rb +329 -0
  65. data/omniauth-azure-activedirectory.gemspec +25 -0
  66. data/spec/fixtures/id_token.txt +1 -0
  67. data/spec/fixtures/id_token_bad_audience.txt +1 -0
  68. data/spec/fixtures/id_token_bad_chash.txt +1 -0
  69. data/spec/fixtures/id_token_bad_issuer.txt +1 -0
  70. data/spec/fixtures/id_token_bad_kid.txt +1 -0
  71. data/spec/fixtures/id_token_bad_nonce.txt +1 -0
  72. data/spec/fixtures/id_token_no_alg.txt +1 -0
  73. data/spec/fixtures/x5c.txt +1 -0
  74. data/spec/fixtures/x5c_different.txt +1 -0
  75. data/spec/omniauth/strategies/azure_activedirectory_spec.rb +222 -0
  76. data/spec/spec_helper.rb +44 -0
  77. metadata +217 -0
@@ -0,0 +1,23 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ require 'omniauth/azure_activedirectory'
@@ -0,0 +1,24 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ require 'omniauth/azure_activedirectory/version'
24
+ require 'omniauth/strategies/azure_activedirectory'
@@ -0,0 +1,28 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ module OmniAuth
24
+ # The release version.
25
+ module AzureActiveDirectory
26
+ VERSION = '1.0.0'
27
+ end
28
+ end
@@ -0,0 +1,329 @@
1
+ #-------------------------------------------------------------------------------
2
+ # Copyright (c) 2015 Micorosft Corporation
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #-------------------------------------------------------------------------------
22
+
23
+ require 'jwt'
24
+ require 'omniauth'
25
+ require 'openssl'
26
+ require 'securerandom'
27
+
28
+ module OmniAuth
29
+ module Strategies
30
+ # A strategy for authentication against Azure Active Directory.
31
+ class AzureActiveDirectory
32
+ include OmniAuth::AzureActiveDirectory
33
+ include OmniAuth::Strategy
34
+
35
+ class OAuthError < StandardError; end
36
+
37
+ ##
38
+ # The client id (key) and tenant must be configured when the OmniAuth
39
+ # middleware is installed. Example:
40
+ #
41
+ # require 'omniauth'
42
+ # require 'omniauth-azure-activedirectory'
43
+ #
44
+ # use OmniAuth::Builder do
45
+ # provider :azure_activedirectory, ENV['AAD_KEY'], ENV['AAD_TENANT']
46
+ # end
47
+ #
48
+ args [:client_id, :tenant]
49
+ option :client_id, nil
50
+ option :tenant, nil
51
+
52
+ # Field renaming is an attempt to fit the OmniAuth recommended schema as
53
+ # best as possible.
54
+ #
55
+ # @see https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
56
+ uid { @claims['sub'] }
57
+ info do
58
+ { name: @claims['name'],
59
+ email: @claims['email'] || @claims['upn'],
60
+ first_name: @claims['given_name'],
61
+ last_name: @claims['family_name'] }
62
+ end
63
+ credentials { { code: @code } }
64
+ extra do
65
+ { session_state: @session_state,
66
+ raw_info:
67
+ { id_token: @id_token,
68
+ id_token_claims: @claims,
69
+ id_token_header: @header } }
70
+ end
71
+
72
+ DEFAULT_RESPONSE_TYPE = 'code id_token'
73
+ DEFAULT_RESPONSE_MODE = 'form_post'
74
+
75
+ ##
76
+ # Overridden method from OmniAuth::Strategy. This is the first step in the
77
+ # authentication process.
78
+ def request_phase
79
+ redirect authorize_endpoint_url
80
+ end
81
+
82
+ ##
83
+ # Overridden method from OmniAuth::Strategy. This is the second step in
84
+ # the authentication process. It is called after the user enters
85
+ # credentials at the authorization endpoint.
86
+ def callback_phase
87
+ error = request.params['error_reason'] || request.params['error']
88
+ fail(OAuthError, error) if error
89
+ @session_state = request.params['session_state']
90
+ @id_token = request.params['id_token']
91
+ @code = request.params['code']
92
+ @claims, @header = validate_and_parse_id_token(@id_token)
93
+ validate_chash(@code, @claims, @header)
94
+ super
95
+ end
96
+
97
+ private
98
+
99
+ ##
100
+ # Constructs a one-time-use authorize_endpoint. This method will use
101
+ # a new nonce on each invocation.
102
+ #
103
+ # @return String
104
+ def authorize_endpoint_url
105
+ uri = URI(openid_config['authorization_endpoint'])
106
+ uri.query = URI.encode_www_form(client_id: client_id,
107
+ redirect_uri: callback_url,
108
+ response_mode: response_mode,
109
+ response_type: response_type,
110
+ nonce: new_nonce)
111
+ uri.to_s
112
+ end
113
+
114
+ ##
115
+ # The client id of the calling application. This must be configured where
116
+ # AzureAD is installed as an OmniAuth strategy.
117
+ #
118
+ # @return String
119
+ def client_id
120
+ return options.client_id if options.client_id
121
+ fail StandardError, 'No client_id specified in AzureAD configuration.'
122
+ end
123
+
124
+ ##
125
+ # The expected id token issuer taken from the discovery endpoint.
126
+ #
127
+ # @return String
128
+ def issuer
129
+ openid_config['issuer']
130
+ end
131
+
132
+ ##
133
+ # Fetches the current signing keys for Azure AD. Note that there should
134
+ # always two available, and that they have a 6 week rollover.
135
+ #
136
+ # Each key is a hash with the following fields:
137
+ # kty, use, kid, x5t, n, e, x5c
138
+ #
139
+ # @return Array[Hash]
140
+ def fetch_signing_keys
141
+ response = JSON.parse(Net::HTTP.get(URI(signing_keys_url)))
142
+ response['keys']
143
+ rescue JSON::ParserError
144
+ raise StandardError, 'Unable to fetch AzureAD signing keys.'
145
+ end
146
+
147
+ ##
148
+ # Fetches the OpenId Connect configuration for the AzureAD tenant. This
149
+ # contains several import values, including:
150
+ #
151
+ # authorization_endpoint
152
+ # token_endpoint
153
+ # token_endpoint_auth_methods_supported
154
+ # jwks_uri
155
+ # response_types_supported
156
+ # response_modes_supported
157
+ # subject_types_supported
158
+ # id_token_signing_alg_values_supported
159
+ # scopes_supported
160
+ # issuer
161
+ # claims_supported
162
+ # microsoft_multi_refresh_token
163
+ # check_session_iframe
164
+ # end_session_endpoint
165
+ # userinfo_endpoint
166
+ #
167
+ # @return Hash
168
+ def fetch_openid_config
169
+ JSON.parse(Net::HTTP.get(URI(openid_config_url)))
170
+ rescue JSON::ParserError
171
+ raise StandardError, 'Unable to fetch OpenId configuration for ' \
172
+ 'AzureAD tenant.'
173
+ end
174
+
175
+ ##
176
+ # Generates a new nonce for one time use. Stores it in the session so
177
+ # multiple users don't share nonces. All nonces should be generated by
178
+ # this method.
179
+ #
180
+ # @return String
181
+ def new_nonce
182
+ session['omniauth-azure-activedirectory.nonce'] = SecureRandom.uuid
183
+ end
184
+
185
+ ##
186
+ # A memoized version of #fetch_openid_config.
187
+ #
188
+ # @return Hash
189
+ def openid_config
190
+ @openid_config ||= fetch_openid_config
191
+ end
192
+
193
+ ##
194
+ # The location of the OpenID configuration for the tenant.
195
+ #
196
+ # @return String
197
+ def openid_config_url
198
+ "https://login.windows.net/#{tenant}/.well-known/openid-configuration"
199
+ end
200
+
201
+ ##
202
+ # Returns the most recent nonce for the session and deletes it from the
203
+ # session.
204
+ #
205
+ # @return String
206
+ def read_nonce
207
+ session.delete('omniauth-azure-activedirectory.nonce')
208
+ end
209
+
210
+ ##
211
+ # The response_type that will be set in the authorization request query
212
+ # parameters. Can be overridden by the client, but it shouldn't need to
213
+ # be.
214
+ #
215
+ # @return String
216
+ def response_type
217
+ options[:response_type] || DEFAULT_RESPONSE_TYPE
218
+ end
219
+
220
+ ##
221
+ # The response_mode that will be set in the authorization request query
222
+ # parameters. Can be overridden by the client, but it shouldn't need to
223
+ # be.
224
+ #
225
+ # @return String
226
+ def response_mode
227
+ options[:response_mode] || DEFAULT_RESPONSE_MODE
228
+ end
229
+
230
+ ##
231
+ # The keys used to sign the id token JWTs. This is just a memoized version
232
+ # of #fetch_signing_keys.
233
+ #
234
+ # @return Array[Hash]
235
+ def signing_keys
236
+ @signing_keys ||= fetch_signing_keys
237
+ end
238
+
239
+ ##
240
+ # The location of the public keys of the token signer. This is parsed from
241
+ # the OpenId config response.
242
+ #
243
+ # @return String
244
+ def signing_keys_url
245
+ return openid_config['jwks_uri'] if openid_config.include? 'jwks_uri'
246
+ fail StandardError, 'No jwks_uri in OpenId config response.'
247
+ end
248
+
249
+ ##
250
+ # The tenant of the calling application. Note that this must be
251
+ # explicitly configured when installing the AzureAD OmniAuth strategy.
252
+ #
253
+ # @return String
254
+ def tenant
255
+ return options.tenant if options.tenant
256
+ fail StandardError, 'No tenant specified in AzureAD configuration.'
257
+ end
258
+
259
+ ##
260
+ # Verifies the signature of the id token as well as the exp, nbf, iat,
261
+ # iss, and aud fields.
262
+ #
263
+ # See OpenId Connect Core 3.1.3.7 and 3.2.2.11.
264
+ #
265
+ # @return Claims, Header
266
+ def validate_and_parse_id_token(id_token)
267
+ # The second parameter is the public key to verify the signature.
268
+ # However, that key is overridden by the value of the executed block
269
+ # if one is present.
270
+ #
271
+ # If you're thinking that this looks ugly with the raw nil and boolean,
272
+ # see https://github.com/jwt/ruby-jwt/issues/59.
273
+ jwt_claims, jwt_header =
274
+ JWT.decode(id_token, nil, true, verify_options) do |header|
275
+ # There should always be one key from the discovery endpoint that
276
+ # matches the id in the JWT header.
277
+ x5c = (signing_keys.find do |key|
278
+ key['kid'] == header['kid']
279
+ end || {})['x5c']
280
+ if x5c.nil? || x5c.empty?
281
+ fail JWT::VerificationError,
282
+ 'No keys from key endpoint match the id token'
283
+ end
284
+ # The key also contains other fields, such as n and e, that are
285
+ # redundant. x5c is sufficient to verify the id token.
286
+ OpenSSL::X509::Certificate.new(JWT.base64url_decode(x5c.first)).public_key
287
+ end
288
+ return jwt_claims, jwt_header if jwt_claims['nonce'] == read_nonce
289
+ fail JWT::DecodeError, 'Returned nonce did not match.'
290
+ end
291
+
292
+ ##
293
+ # Verifies that the c_hash the id token claims matches the authorization
294
+ # code. See OpenId Connect Core 3.3.2.11.
295
+ #
296
+ # @param String code
297
+ # @param Hash claims
298
+ # @param Hash header
299
+ def validate_chash(code, claims, header)
300
+ # This maps RS256 -> sha256, ES384 -> sha384, etc.
301
+ algorithm = (header['alg'] || 'RS256').sub(/RS|ES|HS/, 'sha')
302
+ full_hash = OpenSSL::Digest.new(algorithm).digest code
303
+ c_hash = JWT.base64url_encode full_hash[0..full_hash.length / 2 - 1]
304
+ return if c_hash == claims['c_hash']
305
+ fail JWT::VerificationError,
306
+ 'c_hash in id token does not match auth code.'
307
+ end
308
+
309
+ ##
310
+ # The options passed to the Ruby JWT library to verify the id token.
311
+ # Note that these are not all the checks we perform. Some (like nonce)
312
+ # are not handled by the JWT API and are checked manually in
313
+ # #validate_and_parse_id_token.
314
+ #
315
+ # @return Hash
316
+ def verify_options
317
+ { verify_expiration: true,
318
+ verify_not_before: true,
319
+ verify_iat: true,
320
+ verify_iss: true,
321
+ 'iss' => issuer,
322
+ verify_aud: true,
323
+ 'aud' => client_id }
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ OmniAuth.config.add_camelization 'azure_activedirectory', 'AzureActiveDirectory'
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'omniauth/azure_activedirectory/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'omniauth-azure-activedirectory'
6
+ s.version = OmniAuth::AzureActiveDirectory::VERSION
7
+ s.author = 'Microsoft Corporation'
8
+ s.email = 'nugetaad@microsoft.com'
9
+ s.summary = 'Azure Active Directory strategy for OmniAuth'
10
+ s.description = 'Allows developers to authenticate to AAD'
11
+ s.homepage = 'https://github.com/AzureAD/omniauth-azure-activedirectory'
12
+ s.license = 'MIT'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.require_paths = ['lib']
16
+
17
+ s.add_runtime_dependency 'jwt', '~> 1.5'
18
+ s.add_runtime_dependency 'omniauth', '~> 1.1'
19
+
20
+ s.add_development_dependency 'rake', '~> 10.4'
21
+ s.add_development_dependency 'rspec', '~> 3.3'
22
+ s.add_development_dependency 'rubocop', '~> 0.32'
23
+ s.add_development_dependency 'simplecov', '~> 0.10'
24
+ s.add_development_dependency 'webmock', '~> 1.21'
25
+ end
@@ -0,0 +1 @@
1
+ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiJWcFRRaWk1VF84cmd3eEEtV3RiMkJ3In0.Xz9SL1dm9xeJ3YtBIwSpL7SHEMr5lsL32mkJIugoAt7rNhj2ZauN77_N3skU9FIRTVb_XBFHrLo1AXion7RWoOGAMk8xnuQRlhamGoWsjttWE9oO6J6kuQPSDBvLTA88UqLoGNezDwFNfgUFQq-66m33ZWdkiNOFoFZ_In6DtAwxHZZUys-KoYD3iCbviUoBzU57aV-SBsWyComq39pDGpw03qZoa_xgRfujdVHG1DKlO5VG79kUE3ySYWJyBYVKUdAzjH1iotjpPA1svtqytn4CUldAMi4nnf7iq5SCJMb4ucu0mN6AhJH9ktY--fGY6_OidyiDe4F57ZzLw-3jOQ
@@ -0,0 +1 @@
1
+ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6Im5vdCB0aGUgY2xpZW50IGlkIiwibm9uY2UiOiJteSBub25jZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJzbWl0aCIsImVtYWlsIjoianNtaXRoQGNvbnRvc28uY29tIn0.gGCFTcGDBb2P5tPRj2ojUUiwOJoSslQlxEVTElZY6FCzCZzcypsnrYOB9Adkztp2AWF4wW9fT58lqXwaahKquXtK4wyK4KoNBxXBhS4sDMeNpVkK4914tT_6gecvyUsI_tlJaS0epd5c0mN9-1QjgvNEirY1L-XYaT290LmLYVYIFTEJXoSlnwvv081k0txdJZKr14JXd_bSLUbhGSd-NcGZkJuVmg2F_C65zd1wUsQOkV2iVdJ-ycaDJklv8-DFfDFIHQio8S9yqyieHwArRmTW9HzcoHhWBh2MIItszoTSbmQEF062NtNBPW8gyk1OSot5X9klJUhgPAAFqJ0TJQ
@@ -0,0 +1 @@
1
+ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiI1WEhDdk9qcUgtbnJBXzNXNUJYaWJnIn0.EHPFrxMbr2EH1IZ15F3oP1yvrukadY4ggZSOxm625qPoMxfhv-QzFm0YMhkAG0cLM_LSHi_RgedDwTGuzOtIWqmheYzydnhtbIBeKmx0RSgdob6WeiTZwJ93VRiV4q82Qda41JaaIl_wdWd4lVyVstd9o9jPYMtKEVLgaNDrtHt6pPxEGCVraiaM6tVyKc5XIHu3wNaqle5UZZREU6oirwTCUhXDZkz1g5qY2-aWUdfFVsElSarJuMcxGDPD20hvx7T3D7SCHAF8WhKw3AwQyrodKWCo3cqDOz6vHymb_-ELkJc14GMbtJiyrf6XYySZZoseoI_WBDmLfKcK637xCw
@@ -0,0 +1 @@
1
+ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL3N0cy5pbXBvc3Rlci5uZXQvYnVuY2gtb2YtcmFuZG9tLWNoYXJzIiwibmFtZSI6IkpvaG4gU21pdGgiLCJhdWQiOiJ0aGUgY2xpZW50IGlkIiwibm9uY2UiOiJteSBub25jZSIsImdpdmVuX25hbWUiOiJKb2huIiwiZmFtaWx5X25hbWUiOiJzbWl0aCIsImVtYWlsIjoianNtaXRoQGNvbnRvc28uY29tIn0.fwnJuRsif_Td3MfXoyADHidYJyPFdWSBBoLbVAu4Lz3-pmSln9Vgxl5KowqEKq2LX5n0aqyWVGLcoT-_G_PNXuizuNmssv5vreKLvDMpsFXt2irdwGYDCRki7KQPBk3bn12YjBzE2EqiRy7dTEG_0vWoh1RqoNP72BBL8xYQUlIOFleZhT5KGNYbh7rvcmDq7aA-xdaXT3QJfHCHpitW_zVzZ5Gok_awcdx_v3r3eFbG2IT0PfmT40Ljia0aP2i60KgsOLLHarYO51KFNDEfr1pUDf4IweaEzstbVLwk-_5ulJ5QgByhNJrmWDfrGeCQRk0SO2XX-EUsVn5ySVJ5CQ
@@ -0,0 +1 @@
1
+ eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMzQifQ.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9idW5jaC1vZi1yYW5kb20tY2hhcnMiLCJuYW1lIjoiSm9obiBTbWl0aCIsImF1ZCI6InRoZSBjbGllbnQgaWQiLCJub25jZSI6Im15IG5vbmNlIiwiZ2l2ZW5fbmFtZSI6IkpvaG4iLCJmYW1pbHlfbmFtZSI6InNtaXRoIiwiZW1haWwiOiJqc21pdGhAY29udG9zby5jb20iLCJjX2hhc2giOiI1WEhDdk9qcUgtbnJBXzNXNUJYaWJnIn0.gWGBc9rH30SN17Ikm1CjqIYAyzFHX0yeRQu85sVYLE3r5k26bjS3R6rTJcCQlYqHPRdoPcnkUgT1QVbdThw34ICrODoavs7I5QYEn_jKP9zM4UJEKQaCLBAtitzrk1KDEf0GLcNKif-MYu7MiQfoOzwCGWfIs-vgqk4lv0JUs9OlSLp5LHru7G3jKy6Qswbrpxpjzm9I8BnKEdhUfhz4P6wIf9KLMmhgeGQdQ6wBuxPmOf9r6EKIij2AENhFp1qP90m8kXq9tIt5FZFjwIs_G6spLl0gQXyx0qC8rP5JTkqwrBUieWU-BRqVdax8YxA0iDKzZyfsMV92yVcZT6S_NQ