oauth2 1.4.4 → 1.4.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -2
  3. data/CODE_OF_CONDUCT.md +105 -46
  4. data/README.md +8 -3
  5. data/lib/oauth2/access_token.rb +8 -7
  6. data/lib/oauth2/authenticator.rb +1 -1
  7. data/lib/oauth2/client.rb +60 -14
  8. data/lib/oauth2/mac_token.rb +10 -2
  9. data/lib/oauth2/response.rb +5 -3
  10. data/lib/oauth2/strategy/assertion.rb +3 -3
  11. data/lib/oauth2/strategy/password.rb +2 -2
  12. data/lib/oauth2/version.rb +9 -3
  13. data/spec/helper.rb +37 -0
  14. data/spec/oauth2/access_token_spec.rb +216 -0
  15. data/spec/oauth2/authenticator_spec.rb +84 -0
  16. data/spec/oauth2/client_spec.rb +506 -0
  17. data/spec/oauth2/mac_token_spec.rb +117 -0
  18. data/spec/oauth2/response_spec.rb +90 -0
  19. data/spec/oauth2/strategy/assertion_spec.rb +58 -0
  20. data/spec/oauth2/strategy/auth_code_spec.rb +107 -0
  21. data/spec/oauth2/strategy/base_spec.rb +5 -0
  22. data/spec/oauth2/strategy/client_credentials_spec.rb +69 -0
  23. data/spec/oauth2/strategy/implicit_spec.rb +26 -0
  24. data/spec/oauth2/strategy/password_spec.rb +55 -0
  25. data/spec/oauth2/version_spec.rb +23 -0
  26. metadata +38 -41
  27. data/.document +0 -5
  28. data/.gitignore +0 -19
  29. data/.jrubyrc +0 -1
  30. data/.rspec +0 -2
  31. data/.rubocop.yml +0 -80
  32. data/.rubocop_rspec.yml +0 -26
  33. data/.rubocop_todo.yml +0 -15
  34. data/.ruby-version +0 -1
  35. data/.travis.yml +0 -87
  36. data/CONTRIBUTING.md +0 -18
  37. data/Gemfile +0 -40
  38. data/Rakefile +0 -45
  39. data/gemfiles/jruby_1.7.gemfile +0 -11
  40. data/gemfiles/jruby_9.0.gemfile +0 -7
  41. data/gemfiles/jruby_9.1.gemfile +0 -3
  42. data/gemfiles/jruby_9.2.gemfile +0 -3
  43. data/gemfiles/jruby_head.gemfile +0 -3
  44. data/gemfiles/ruby_1.9.gemfile +0 -11
  45. data/gemfiles/ruby_2.0.gemfile +0 -6
  46. data/gemfiles/ruby_2.1.gemfile +0 -6
  47. data/gemfiles/ruby_2.2.gemfile +0 -3
  48. data/gemfiles/ruby_2.3.gemfile +0 -3
  49. data/gemfiles/ruby_2.4.gemfile +0 -3
  50. data/gemfiles/ruby_2.5.gemfile +0 -3
  51. data/gemfiles/ruby_2.6.gemfile +0 -9
  52. data/gemfiles/ruby_2.7.gemfile +0 -9
  53. data/gemfiles/ruby_head.gemfile +0 -9
  54. data/gemfiles/truffleruby.gemfile +0 -3
  55. data/oauth2.gemspec +0 -52
@@ -0,0 +1,506 @@
1
+ # coding: utf-8
2
+
3
+ require 'helper'
4
+ require 'nkf'
5
+
6
+ describe OAuth2::Client do
7
+ subject do
8
+ described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
9
+ builder.adapter :test do |stub|
10
+ stub.get('/success') { |env| [200, {'Content-Type' => 'text/awesome'}, 'yay'] }
11
+ stub.get('/reflect') { |env| [200, {}, env[:body]] }
12
+ stub.post('/reflect') { |env| [200, {}, env[:body]] }
13
+ stub.get('/unauthorized') { |env| [401, {'Content-Type' => 'application/json'}, MultiJson.encode(:error => error_value, :error_description => error_description_value)] }
14
+ stub.get('/conflict') { |env| [409, {'Content-Type' => 'text/plain'}, 'not authorized'] }
15
+ stub.get('/redirect') { |env| [302, {'Content-Type' => 'text/plain', 'location' => '/success'}, ''] }
16
+ stub.post('/redirect') { |env| [303, {'Content-Type' => 'text/plain', 'location' => '/reflect'}, ''] }
17
+ stub.get('/error') { |env| [500, {'Content-Type' => 'text/plain'}, 'unknown error'] }
18
+ stub.get('/empty_get') { |env| [204, {}, nil] }
19
+ stub.get('/different_encoding') { |env| [500, {'Content-Type' => 'application/json'}, NKF.nkf('-We', MultiJson.encode(:error => error_value, :error_description => '∞'))] }
20
+ stub.get('/ascii_8bit_encoding') { |env| [500, {'Content-Type' => 'application/json'}, MultiJson.encode(:error => 'invalid_request', :error_description => 'é').force_encoding('ASCII-8BIT')] }
21
+ end
22
+ end
23
+ end
24
+
25
+ let!(:error_value) { 'invalid_token' }
26
+ let!(:error_description_value) { 'bad bad token' }
27
+
28
+ describe '#initialize' do
29
+ it 'assigns id and secret' do
30
+ expect(subject.id).to eq('abc')
31
+ expect(subject.secret).to eq('def')
32
+ end
33
+
34
+ it 'assigns site from the options hash' do
35
+ expect(subject.site).to eq('https://api.example.com')
36
+ end
37
+
38
+ it 'assigns Faraday::Connection#host' do
39
+ expect(subject.connection.host).to eq('api.example.com')
40
+ end
41
+
42
+ it 'leaves Faraday::Connection#ssl unset' do
43
+ expect(subject.connection.ssl).to be_empty
44
+ end
45
+
46
+ it 'is able to pass a block to configure the connection' do
47
+ connection = double('connection')
48
+ builder = double('builder')
49
+ allow(connection).to receive(:build).and_yield(builder)
50
+ allow(Faraday::Connection).to receive(:new).and_return(connection)
51
+
52
+ expect(builder).to receive(:adapter).with(:test)
53
+
54
+ described_class.new('abc', 'def') do |client|
55
+ client.adapter :test
56
+ end.connection
57
+ end
58
+
59
+ it 'defaults raise_errors to true' do
60
+ expect(subject.options[:raise_errors]).to be true
61
+ end
62
+
63
+ it 'allows true/false for raise_errors option' do
64
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => false)
65
+ expect(client.options[:raise_errors]).to be false
66
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => true)
67
+ expect(client.options[:raise_errors]).to be true
68
+ end
69
+
70
+ it 'allows override of raise_errors option' do
71
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => true) do |builder|
72
+ builder.adapter :test do |stub|
73
+ stub.get('/notfound') { |env| [404, {}, nil] }
74
+ end
75
+ end
76
+ expect(client.options[:raise_errors]).to be true
77
+ expect { client.request(:get, '/notfound') }.to raise_error(OAuth2::Error)
78
+ response = client.request(:get, '/notfound', :raise_errors => false)
79
+ expect(response.status).to eq(404)
80
+ end
81
+
82
+ it 'allows get/post for access_token_method option' do
83
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :access_token_method => :get)
84
+ expect(client.options[:access_token_method]).to eq(:get)
85
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :access_token_method => :post)
86
+ expect(client.options[:access_token_method]).to eq(:post)
87
+ end
88
+
89
+ it 'does not mutate the opts hash argument' do
90
+ opts = {:site => 'http://example.com/'}
91
+ opts2 = opts.dup
92
+ described_class.new 'abc', 'def', opts
93
+ expect(opts).to eq(opts2)
94
+ end
95
+ end
96
+
97
+ %w[authorize token].each do |url_type|
98
+ describe ":#{url_type}_url option" do
99
+ it "defaults to a path of /oauth/#{url_type}" do
100
+ expect(subject.send("#{url_type}_url")).to eq("https://api.example.com/oauth/#{url_type}")
101
+ end
102
+
103
+ it "is settable via the :#{url_type}_url option" do
104
+ subject.options[:"#{url_type}_url"] = '/oauth/custom'
105
+ expect(subject.send("#{url_type}_url")).to eq('https://api.example.com/oauth/custom')
106
+ end
107
+
108
+ it 'allows a different host than the site' do
109
+ subject.options[:"#{url_type}_url"] = 'https://api.foo.com/oauth/custom'
110
+ expect(subject.send("#{url_type}_url")).to eq('https://api.foo.com/oauth/custom')
111
+ end
112
+ end
113
+ end
114
+
115
+ describe ':redirect_uri option' do
116
+ let(:auth_code_params) do
117
+ {
118
+ 'client_id' => 'abc',
119
+ 'client_secret' => 'def',
120
+ 'code' => 'code',
121
+ 'grant_type' => 'authorization_code',
122
+ }
123
+ end
124
+
125
+ context 'when blank' do
126
+ it 'there is no redirect_uri param added to authorization URL' do
127
+ expect(subject.authorize_url('a' => 'b')).to eq('https://api.example.com/oauth/authorize?a=b')
128
+ end
129
+
130
+ it 'does not add the redirect_uri param to the auth_code token exchange request' do
131
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
132
+ builder.adapter :test do |stub|
133
+ stub.post('/oauth/token', auth_code_params) do
134
+ [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
135
+ end
136
+ end
137
+ end
138
+ client.auth_code.get_token('code')
139
+ end
140
+ end
141
+
142
+ context 'when set' do
143
+ before { subject.options[:redirect_uri] = 'https://site.com/oauth/callback' }
144
+
145
+ it 'adds the redirect_uri param to authorization URL' do
146
+ expect(subject.authorize_url('a' => 'b')).to eq('https://api.example.com/oauth/authorize?a=b&redirect_uri=https%3A%2F%2Fsite.com%2Foauth%2Fcallback')
147
+ end
148
+
149
+ it 'adds the redirect_uri param to the auth_code token exchange request' do
150
+ client = described_class.new('abc', 'def', :redirect_uri => 'https://site.com/oauth/callback', :site => 'https://api.example.com') do |builder|
151
+ builder.adapter :test do |stub|
152
+ stub.post('/oauth/token', auth_code_params.merge('redirect_uri' => 'https://site.com/oauth/callback')) do
153
+ [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
154
+ end
155
+ end
156
+ end
157
+ client.auth_code.get_token('code')
158
+ end
159
+ end
160
+
161
+ describe 'custom headers' do
162
+ context 'string key headers' do
163
+ it 'adds the custom headers to request' do
164
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :auth_scheme => :request_body) do |builder|
165
+ builder.adapter :test do |stub|
166
+ stub.post('/oauth/token') do |env|
167
+ expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
168
+ [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
169
+ end
170
+ end
171
+ end
172
+ header_params = {'headers' => {'CustomHeader' => 'CustomHeader'}}
173
+ client.auth_code.get_token('code', header_params)
174
+ end
175
+ end
176
+
177
+ context 'symbol key headers' do
178
+ it 'adds the custom headers to request' do
179
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com', :auth_scheme => :request_body) do |builder|
180
+ builder.adapter :test do |stub|
181
+ stub.post('/oauth/token') do |env|
182
+ expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
183
+ [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
184
+ end
185
+ end
186
+ end
187
+ header_params = {:headers => {'CustomHeader' => 'CustomHeader'}}
188
+ client.auth_code.get_token('code', header_params)
189
+ end
190
+ end
191
+
192
+ context 'string key custom headers with basic auth' do
193
+ it 'adds the custom headers to request' do
194
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
195
+ builder.adapter :test do |stub|
196
+ stub.post('/oauth/token') do |env|
197
+ expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
198
+ [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
199
+ end
200
+ end
201
+ end
202
+ header_params = {'headers' => {'CustomHeader' => 'CustomHeader'}}
203
+ client.auth_code.get_token('code', header_params)
204
+ end
205
+ end
206
+
207
+ context 'symbol key custom headers with basic auth' do
208
+ it 'adds the custom headers to request' do
209
+ client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
210
+ builder.adapter :test do |stub|
211
+ stub.post('/oauth/token') do |env|
212
+ expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
213
+ [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
214
+ end
215
+ end
216
+ end
217
+ header_params = {:headers => {'CustomHeader' => 'CustomHeader'}}
218
+ client.auth_code.get_token('code', header_params)
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ describe '#request' do
225
+ it 'works with a null response body' do
226
+ expect(subject.request(:get, 'empty_get').body).to eq('')
227
+ end
228
+
229
+ it 'returns on a successful response' do
230
+ response = subject.request(:get, '/success')
231
+ expect(response.body).to eq('yay')
232
+ expect(response.status).to eq(200)
233
+ expect(response.headers).to eq('Content-Type' => 'text/awesome')
234
+ end
235
+
236
+ it 'posts a body' do
237
+ response = subject.request(:post, '/reflect', :body => 'foo=bar')
238
+ expect(response.body).to eq('foo=bar')
239
+ end
240
+
241
+ it 'follows redirects properly' do
242
+ response = subject.request(:get, '/redirect')
243
+ expect(response.body).to eq('yay')
244
+ expect(response.status).to eq(200)
245
+ expect(response.headers).to eq('Content-Type' => 'text/awesome')
246
+ end
247
+
248
+ it 'redirects using GET on a 303' do
249
+ response = subject.request(:post, '/redirect', :body => 'foo=bar')
250
+ expect(response.body).to be_empty
251
+ expect(response.status).to eq(200)
252
+ end
253
+
254
+ it 'obeys the :max_redirects option' do
255
+ max_redirects = subject.options[:max_redirects]
256
+ subject.options[:max_redirects] = 0
257
+ response = subject.request(:get, '/redirect')
258
+ expect(response.status).to eq(302)
259
+ subject.options[:max_redirects] = max_redirects
260
+ end
261
+
262
+ it 'returns if raise_errors is false' do
263
+ subject.options[:raise_errors] = false
264
+ response = subject.request(:get, '/unauthorized')
265
+
266
+ expect(response.status).to eq(401)
267
+ expect(response.headers).to eq('Content-Type' => 'application/json')
268
+ expect(response.error).not_to be_nil
269
+ end
270
+
271
+ %w[/unauthorized /conflict /error /different_encoding /ascii_8bit_encoding].each do |error_path|
272
+ it "raises OAuth2::Error on error response to path #{error_path}" do
273
+ expect { subject.request(:get, error_path) }.to raise_error(OAuth2::Error)
274
+ end
275
+ end
276
+
277
+ # rubocop:disable Style/RedundantBegin
278
+ it 're-encodes response body in the error message' do
279
+ begin
280
+ subject.request(:get, '/ascii_8bit_encoding')
281
+ rescue StandardError => e
282
+ expect(e.message.encoding.name).to eq('UTF-8')
283
+ expect(e.message).to eq("invalid_request: é\n{\"error\":\"invalid_request\",\"error_description\":\"��\"}")
284
+ end
285
+ end
286
+
287
+ it 'parses OAuth2 standard error response' do
288
+ begin
289
+ subject.request(:get, '/unauthorized')
290
+ rescue StandardError => e
291
+ expect(e.code).to eq(error_value)
292
+ expect(e.description).to eq(error_description_value)
293
+ expect(e.to_s).to match(/#{error_value}/)
294
+ expect(e.to_s).to match(/#{error_description_value}/)
295
+ end
296
+ end
297
+
298
+ it 'provides the response in the Exception' do
299
+ begin
300
+ subject.request(:get, '/error')
301
+ rescue StandardError => e
302
+ expect(e.response).not_to be_nil
303
+ expect(e.to_s).to match(/unknown error/)
304
+ end
305
+ end
306
+ # rubocop:enable Style/RedundantBegin
307
+
308
+ context 'with ENV' do
309
+ include_context 'with stubbed env'
310
+ before do
311
+ stub_env('OAUTH_DEBUG' => 'true')
312
+ end
313
+
314
+ it 'outputs to $stdout when OAUTH_DEBUG=true' do
315
+ output = capture(:stdout) do
316
+ subject.request(:get, '/success')
317
+ end
318
+ logs = [
319
+ '-- request: GET https://api.example.com/success',
320
+ '-- response: Status 200',
321
+ '-- response: Content-Type: "text/awesome"',
322
+ ]
323
+ expect(output).to include(*logs)
324
+ end
325
+ end
326
+ end
327
+
328
+ describe '#get_token' do
329
+ it 'returns a configured AccessToken' do
330
+ client = stubbed_client do |stub|
331
+ stub.post('/oauth/token') do
332
+ [200, {'Content-Type' => 'application/json'}, MultiJson.encode('access_token' => 'the-token')]
333
+ end
334
+ end
335
+
336
+ token = client.get_token({})
337
+ expect(token).to be_a OAuth2::AccessToken
338
+ expect(token.token).to eq('the-token')
339
+ end
340
+
341
+ it 'authenticates with request parameters' do
342
+ client = stubbed_client(:auth_scheme => :request_body) do |stub|
343
+ stub.post('/oauth/token', 'client_id' => 'abc', 'client_secret' => 'def') do |env|
344
+ [200, {'Content-Type' => 'application/json'}, MultiJson.encode('access_token' => 'the-token')]
345
+ end
346
+ end
347
+ client.get_token({})
348
+ end
349
+
350
+ it 'authenticates with Basic auth' do
351
+ client = stubbed_client(:auth_scheme => :basic_auth) do |stub|
352
+ stub.post('/oauth/token') do |env|
353
+ raise Faraday::Adapter::Test::Stubs::NotFound unless env[:request_headers]['Authorization'] == OAuth2::Authenticator.encode_basic_auth('abc', 'def')
354
+
355
+ [200, {'Content-Type' => 'application/json'}, MultiJson.encode('access_token' => 'the-token')]
356
+ end
357
+ end
358
+ client.get_token({})
359
+ end
360
+
361
+ describe 'extract_access_token option' do
362
+ let(:client) do
363
+ client = stubbed_client(:extract_access_token => extract_access_token) do |stub|
364
+ stub.post('/oauth/token') do
365
+ [200, {'Content-Type' => 'application/json'}, MultiJson.encode('data' => {'access_token' => 'the-token'})]
366
+ end
367
+ end
368
+ end
369
+
370
+ context 'with proc extract_access_token' do
371
+ let(:extract_access_token) do
372
+ proc do |client, hash|
373
+ token = hash['data']['access_token']
374
+ OAuth2::AccessToken.new(client, token, hash)
375
+ end
376
+ end
377
+
378
+ it 'returns a configured AccessToken' do
379
+ token = client.get_token({})
380
+ expect(token).to be_a OAuth2::AccessToken
381
+ expect(token.token).to eq('the-token')
382
+ end
383
+ end
384
+
385
+ context 'with depracted Class.from_hash option' do
386
+ let(:extract_access_token) do
387
+ CustomAccessToken = Class.new(OAuth2::AccessToken)
388
+ CustomAccessToken.define_singleton_method(:from_hash) do |client, hash|
389
+ token = hash['data']['access_token']
390
+ OAuth2::AccessToken.new(client, token, hash)
391
+ end
392
+ CustomAccessToken
393
+ end
394
+
395
+ it 'returns a configured AccessToken' do
396
+ token = client.get_token({})
397
+ expect(token).to be_a OAuth2::AccessToken
398
+ expect(token.token).to eq('the-token')
399
+ end
400
+ end
401
+ end
402
+
403
+ describe ':raise_errors flag' do
404
+ let(:options) { {} }
405
+ let(:token_response) { nil }
406
+
407
+ let(:client) do
408
+ stubbed_client(options.merge(:raise_errors => raise_errors)) do |stub|
409
+ stub.post('/oauth/token') do
410
+ # stub 200 response so that we're testing the get_token handling of :raise_errors flag not request
411
+ [200, {'Content-Type' => 'application/json'}, token_response]
412
+ end
413
+ end
414
+ end
415
+
416
+ context 'when set to false' do
417
+ let(:raise_errors) { false }
418
+
419
+ context 'when the request body is nil' do
420
+ it 'returns a nil :access_token' do
421
+ expect(client.get_token({})).to eq(nil)
422
+ end
423
+ end
424
+
425
+ context 'when the request body is missing the access_token' do
426
+ let(:token_response) { MultiJson.encode('unexpected_access_token' => 'the-token') }
427
+
428
+ it 'returns a nil :access_token' do
429
+ expect(client.get_token({})).to eq(nil)
430
+ end
431
+ end
432
+
433
+ context 'when extract_access_token raises an exception' do
434
+ let(:options) do
435
+ {
436
+ :extract_access_token => proc { |client, hash| raise ArgumentError },
437
+ }
438
+ end
439
+
440
+ it 'returns a nil :access_token' do
441
+ expect(client.get_token({})).to eq(nil)
442
+ end
443
+ end
444
+ end
445
+
446
+ context 'when set to true' do
447
+ let(:raise_errors) { true }
448
+
449
+ context 'when the request body is nil' do
450
+ it 'raises an error' do
451
+ expect { client.get_token({}) }.to raise_error OAuth2::Error
452
+ end
453
+ end
454
+
455
+ context 'when the request body is missing the access_token' do
456
+ let(:token_response) { MultiJson.encode('unexpected_access_token' => 'the-token') }
457
+
458
+ it 'raises an error' do
459
+ expect { client.get_token({}) }.to raise_error OAuth2::Error
460
+ end
461
+ end
462
+
463
+ context 'when extract_access_token raises an exception' do
464
+ let(:options) do
465
+ {
466
+ :extract_access_token => proc { |client, hash| raise ArgumentError },
467
+ }
468
+ end
469
+
470
+ it 'raises an error' do
471
+ expect { client.get_token({}) }.to raise_error OAuth2::Error
472
+ end
473
+ end
474
+ end
475
+ end
476
+
477
+ def stubbed_client(params = {}, &stubs)
478
+ params = {:site => 'https://api.example.com'}.merge(params)
479
+ OAuth2::Client.new('abc', 'def', params) do |builder|
480
+ builder.adapter :test, &stubs
481
+ end
482
+ end
483
+ end
484
+
485
+ it 'instantiates an AuthCode strategy with this client' do
486
+ expect(subject.auth_code).to be_kind_of(OAuth2::Strategy::AuthCode)
487
+ end
488
+
489
+ it 'instantiates an Implicit strategy with this client' do
490
+ expect(subject.implicit).to be_kind_of(OAuth2::Strategy::Implicit)
491
+ end
492
+
493
+ context 'with SSL options' do
494
+ subject do
495
+ cli = described_class.new('abc', 'def', :site => 'https://api.example.com', :ssl => {:ca_file => 'foo.pem'})
496
+ cli.connection.build do |b|
497
+ b.adapter :test
498
+ end
499
+ cli
500
+ end
501
+
502
+ it 'passes the SSL options along to Faraday::Connection#ssl' do
503
+ expect(subject.connection.ssl.fetch(:ca_file)).to eq('foo.pem')
504
+ end
505
+ end
506
+ end