oauth2 1.4.6 → 1.4.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -1
  3. data/README.md +1 -0
  4. data/lib/oauth2/access_token.rb +3 -5
  5. data/lib/oauth2/version.rb +7 -3
  6. data/spec/helper.rb +37 -0
  7. data/spec/oauth2/access_token_spec.rb +216 -0
  8. data/spec/oauth2/authenticator_spec.rb +84 -0
  9. data/spec/oauth2/client_spec.rb +506 -0
  10. data/spec/oauth2/mac_token_spec.rb +117 -0
  11. data/spec/oauth2/response_spec.rb +90 -0
  12. data/spec/oauth2/strategy/assertion_spec.rb +58 -0
  13. data/spec/oauth2/strategy/auth_code_spec.rb +107 -0
  14. data/spec/oauth2/strategy/base_spec.rb +5 -0
  15. data/spec/oauth2/strategy/client_credentials_spec.rb +69 -0
  16. data/spec/oauth2/strategy/implicit_spec.rb +26 -0
  17. data/spec/oauth2/strategy/password_spec.rb +55 -0
  18. data/spec/oauth2/version_spec.rb +23 -0
  19. metadata +31 -31
  20. data/.document +0 -5
  21. data/.github/dependabot.yml +0 -8
  22. data/.github/workflows/style.yml +0 -37
  23. data/.github/workflows/test.yml +0 -58
  24. data/.gitignore +0 -19
  25. data/.jrubyrc +0 -1
  26. data/.rspec +0 -4
  27. data/.rubocop.yml +0 -112
  28. data/.rubocop_rspec.yml +0 -26
  29. data/.rubocop_todo.yml +0 -113
  30. data/.ruby-version +0 -1
  31. data/.travis.yml +0 -75
  32. data/CONTRIBUTING.md +0 -18
  33. data/Gemfile +0 -61
  34. data/Rakefile +0 -45
  35. data/gemfiles/jruby_1.7.gemfile +0 -11
  36. data/gemfiles/jruby_9.0.gemfile +0 -7
  37. data/gemfiles/jruby_9.1.gemfile +0 -3
  38. data/gemfiles/jruby_9.2.gemfile +0 -3
  39. data/gemfiles/jruby_head.gemfile +0 -3
  40. data/gemfiles/ruby_1.9.gemfile +0 -11
  41. data/gemfiles/ruby_2.0.gemfile +0 -6
  42. data/gemfiles/ruby_head.gemfile +0 -9
  43. data/gemfiles/truffleruby.gemfile +0 -3
  44. data/maintenance-branch +0 -1
  45. data/oauth2.gemspec +0 -52
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7912a62b5b26c6c8cdedaf620194ef0d2a0adbd92307da9fe29b77d88c984b34
4
- data.tar.gz: 83bd964f93de41cd8ede2e8fdcf9aa037ff5d779ba7d922589d5cb666fe8c1c1
3
+ metadata.gz: 74ca15f9b1935885fe123c9b0801b9f0605c1bfa904bebe371d201a61b559f31
4
+ data.tar.gz: 3b719ec6748493cba5112dfca8ebff22b13c0c9690330d4422f9d81b1abf5185
5
5
  SHA512:
6
- metadata.gz: 4b18270faea191ac365fc6c63e350bd94a4180d1d64966c2eb6f59c2fd3131e7175df62e8179dba257661447fa7ec4d222ba166ee5ddab9fec4b29340629ed81
7
- data.tar.gz: 8bf6ed52a51b03aba99af5ea24c63a13a6f548708ac05e88f4c19a6813211d01bacc1f60b0505ad7bba1e21425b6ff7817dee2cf1424605eed27b1aaf76e86eb
6
+ metadata.gz: 7ec3c9c311effac7f8864c2dc3d7332761ee6a986f2924adec3b77620abe3dc63984d1c01e9ca0a1283907a5b2ba23ee31dfd4bd630fa7d803fba116ba1e652d
7
+ data.tar.gz: 1e1d37d5953bdd7e03e13f697648e56afdb63ea54bbdbaee7ee229302149c5047c533a15d6498a355ef78d17d991d79ce8f94fe8728229431e3fad5c3132a9ad
data/CHANGELOG.md CHANGED
@@ -3,9 +3,13 @@ All notable changes to this project will be documented in this file.
3
3
 
4
4
  ## unreleased
5
5
 
6
- ## [1.4.6] - 2021-03-18
6
+ ## [1.4.7] - 2021-03-18
7
+
8
+ - [#541](https://github.com/oauth-xx/oauth2/pull/541) - Backport fix to expires_at handling [#533](https://github.com/oauth-xx/oauth2/pull/533) to 1-4-stable branch. (@dobon)
7
9
 
10
+ ## [1.4.6] - 2021-03-18
8
11
 
12
+ - [#540](https://github.com/oauth-xx/oauth2/pull/540) - Add VERSION constant (@pboling)
9
13
  - [#537](https://github.com/oauth-xx/oauth2/pull/537) - Fix crash in OAuth2::Client#get_token (@anderscarling)
10
14
  - [#538](https://github.com/oauth-xx/oauth2/pull/538) - Remove reliance on globally included OAuth2 in tests for version 1.4 (@anderscarling)
11
15
 
data/README.md CHANGED
@@ -4,6 +4,7 @@ If you need the readme for a released version of the gem please find it below:
4
4
 
5
5
  | Version | Release Date | Readme |
6
6
  |----------|--------------|----------------------------------------------------------|
7
+ | 1.4.7 | Mar 18, 2021 | https://github.com/oauth-xx/oauth2/blob/v1.4.7/README.md |
7
8
  | 1.4.6 | Mar 18, 2021 | https://github.com/oauth-xx/oauth2/blob/v1.4.6/README.md |
8
9
  | 1.4.5 | Mar 18, 2021 | https://github.com/oauth-xx/oauth2/blob/v1.4.5/README.md |
9
10
  | 1.4.4 | Feb 12, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.4/README.md |
@@ -173,11 +173,9 @@ module OAuth2
173
173
  end
174
174
 
175
175
  def convert_expires_at(expires_at)
176
- expires_at_i = expires_at.to_i
177
- return expires_at_i if expires_at_i > Time.now.utc.to_i
178
- return Time.parse(expires_at).to_i if expires_at.is_a?(String)
179
-
180
- expires_at_i
176
+ Time.iso8601(expires_at.to_s).to_i
177
+ rescue ArgumentError
178
+ expires_at.to_i
181
179
  end
182
180
  end
183
181
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Version
3
5
  VERSION = to_s
@@ -22,12 +24,12 @@ module OAuth2
22
24
  #
23
25
  # @return [Integer]
24
26
  def patch
25
- 6
27
+ 7
26
28
  end
27
29
 
28
30
  # The pre-release version, if any
29
31
  #
30
- # @return [Integer, NilClass]
32
+ # @return [String, NilClass]
31
33
  def pre
32
34
  nil
33
35
  end
@@ -55,7 +57,9 @@ module OAuth2
55
57
  #
56
58
  # @return [String]
57
59
  def to_s
58
- to_a.join('.')
60
+ v = [major, minor, patch].compact.join('.')
61
+ v += "-#{pre}" if pre
62
+ v
59
63
  end
60
64
  end
61
65
  end
data/spec/helper.rb ADDED
@@ -0,0 +1,37 @@
1
+ DEBUG = ENV['DEBUG'] == 'true'
2
+
3
+ ruby_version = Gem::Version.new(RUBY_VERSION)
4
+
5
+ if ruby_version >= Gem::Version.new('2.7')
6
+ require 'simplecov'
7
+ require 'coveralls'
8
+
9
+ SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
10
+
11
+ SimpleCov.start do
12
+ add_filter '/spec'
13
+ minimum_coverage(95)
14
+ end
15
+ end
16
+
17
+ require 'byebug' if DEBUG && ruby_version >= Gem::Version.new('2.4')
18
+
19
+ require 'oauth2'
20
+ require 'addressable/uri'
21
+ require 'rspec'
22
+ require 'rspec/stubbed_env'
23
+ require 'silent_stream'
24
+
25
+ RSpec.configure do |config|
26
+ config.expect_with :rspec do |c|
27
+ c.syntax = :expect
28
+ end
29
+ end
30
+
31
+ Faraday.default_adapter = :test
32
+
33
+ RSpec.configure do |conf|
34
+ conf.include SilentStream
35
+ end
36
+
37
+ VERBS = [:get, :post, :put, :delete].freeze
@@ -0,0 +1,216 @@
1
+ describe OAuth2::AccessToken do
2
+ subject { described_class.new(client, token) }
3
+
4
+ let(:token) { 'monkey' }
5
+ let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => 'refresh_bar') }
6
+ let(:client) do
7
+ OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
8
+ builder.request :url_encoded
9
+ builder.adapter :test do |stub|
10
+ VERBS.each do |verb|
11
+ stub.send(verb, '/token/header') { |env| [200, {}, env[:request_headers]['Authorization']] }
12
+ stub.send(verb, "/token/query?access_token=#{token}") { |env| [200, {}, Addressable::URI.parse(env[:url]).query_values['access_token']] }
13
+ stub.send(verb, '/token/query_string') { |env| [200, {}, CGI.unescape(Addressable::URI.parse(env[:url]).query)] }
14
+ stub.send(verb, '/token/body') { |env| [200, {}, env[:body]] }
15
+ end
16
+ stub.post('/oauth/token') { |env| [200, {'Content-Type' => 'application/json'}, refresh_body] }
17
+ end
18
+ end
19
+ end
20
+
21
+ describe '#initialize' do
22
+ it 'assigns client and token' do
23
+ expect(subject.client).to eq(client)
24
+ expect(subject.token).to eq(token)
25
+ end
26
+
27
+ it 'assigns extra params' do
28
+ target = described_class.new(client, token, 'foo' => 'bar')
29
+ expect(target.params).to include('foo')
30
+ expect(target.params['foo']).to eq('bar')
31
+ end
32
+
33
+ def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize
34
+ expect(target.token).to eq(token)
35
+ expect(target).to be_expires
36
+ expect(target.params.keys).to include('foo')
37
+ expect(target.params['foo']).to eq('bar')
38
+ end
39
+
40
+ it 'initializes with a Hash' do
41
+ hash = {:access_token => token, :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
42
+ target = described_class.from_hash(client, hash)
43
+ assert_initialized_token(target)
44
+ end
45
+
46
+ it 'from_hash does not modify opts hash' do
47
+ hash = {:access_token => token, :expires_at => Time.now.to_i}
48
+ hash_before = hash.dup
49
+ described_class.from_hash(client, hash)
50
+ expect(hash).to eq(hash_before)
51
+ end
52
+
53
+ it 'initializes with a form-urlencoded key/value string' do
54
+ kvform = "access_token=#{token}&expires_at=#{Time.now.to_i + 200}&foo=bar"
55
+ target = described_class.from_kvform(client, kvform)
56
+ assert_initialized_token(target)
57
+ end
58
+
59
+ it 'sets options' do
60
+ target = described_class.new(client, token, :param_name => 'foo', :header_format => 'Bearer %', :mode => :body)
61
+ expect(target.options[:param_name]).to eq('foo')
62
+ expect(target.options[:header_format]).to eq('Bearer %')
63
+ expect(target.options[:mode]).to eq(:body)
64
+ end
65
+
66
+ it 'does not modify opts hash' do
67
+ opts = {:param_name => 'foo', :header_format => 'Bearer %', :mode => :body}
68
+ opts_before = opts.dup
69
+ described_class.new(client, token, opts)
70
+ expect(opts).to eq(opts_before)
71
+ end
72
+
73
+ describe 'expires_at' do
74
+ let(:expires_at) { 1_361_396_829 }
75
+ let(:hash) do
76
+ {
77
+ :access_token => token,
78
+ :expires_at => expires_at.to_s,
79
+ 'foo' => 'bar',
80
+ }
81
+ end
82
+
83
+ it 'initializes with an integer timestamp expires_at' do
84
+ target = described_class.from_hash(client, hash.merge(:expires_at => expires_at))
85
+ assert_initialized_token(target)
86
+ expect(target.expires_at).to eql(expires_at)
87
+ end
88
+
89
+ it 'initializes with a string timestamp expires_at' do
90
+ target = described_class.from_hash(client, hash)
91
+ assert_initialized_token(target)
92
+ expect(target.expires_at).to eql(expires_at)
93
+ end
94
+
95
+ it 'initializes with a string time expires_at' do
96
+ target = described_class.from_hash(client, hash.merge(:expires_at => Time.at(expires_at).iso8601))
97
+ assert_initialized_token(target)
98
+ expect(target.expires_at).to eql(expires_at)
99
+ end
100
+ end
101
+ end
102
+
103
+ describe '#request' do
104
+ context 'with :mode => :header' do
105
+ before do
106
+ subject.options[:mode] = :header
107
+ end
108
+
109
+ VERBS.each do |verb|
110
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
111
+ expect(subject.post('/token/header').body).to include(token)
112
+ end
113
+ end
114
+ end
115
+
116
+ context 'with :mode => :query' do
117
+ before do
118
+ subject.options[:mode] = :query
119
+ end
120
+
121
+ VERBS.each do |verb|
122
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
123
+ expect(subject.post('/token/query').body).to eq(token)
124
+ end
125
+
126
+ it "sends a #{verb.to_s.upcase} request and options[:param_name] include [number]." do
127
+ subject.options[:param_name] = 'auth[1]'
128
+ expect(subject.__send__(verb, '/token/query_string').body).to include("auth[1]=#{token}")
129
+ end
130
+ end
131
+ end
132
+
133
+ context 'with :mode => :body' do
134
+ before do
135
+ subject.options[:mode] = :body
136
+ end
137
+
138
+ VERBS.each do |verb|
139
+ it "sends the token in the Authorization header for a #{verb.to_s.upcase} request" do
140
+ expect(subject.post('/token/body').body.split('=').last).to eq(token)
141
+ end
142
+ end
143
+ end
144
+
145
+ context 'params include [number]' do
146
+ VERBS.each do |verb|
147
+ it "sends #{verb.to_s.upcase} correct query" do
148
+ expect(subject.__send__(verb, '/token/query_string', :params => {'foo[bar][1]' => 'val'}).body).to include('foo[bar][1]=val')
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ describe '#expires?' do
155
+ it 'is false if there is no expires_at' do
156
+ expect(described_class.new(client, token)).not_to be_expires
157
+ end
158
+
159
+ it 'is true if there is an expires_in' do
160
+ expect(described_class.new(client, token, :refresh_token => 'abaca', :expires_in => 600)).to be_expires
161
+ end
162
+
163
+ it 'is true if there is an expires_at' do
164
+ expect(described_class.new(client, token, :refresh_token => 'abaca', :expires_in => Time.now.getutc.to_i + 600)).to be_expires
165
+ end
166
+ end
167
+
168
+ describe '#expired?' do
169
+ it 'is false if there is no expires_in or expires_at' do
170
+ expect(described_class.new(client, token)).not_to be_expired
171
+ end
172
+
173
+ it 'is false if expires_in is in the future' do
174
+ expect(described_class.new(client, token, :refresh_token => 'abaca', :expires_in => 10_800)).not_to be_expired
175
+ end
176
+
177
+ it 'is true if expires_at is in the past' do
178
+ access = described_class.new(client, token, :refresh_token => 'abaca', :expires_in => 600)
179
+ @now = Time.now + 10_800
180
+ allow(Time).to receive(:now).and_return(@now)
181
+ expect(access).to be_expired
182
+ end
183
+ end
184
+
185
+ describe '#refresh!' do
186
+ let(:access) do
187
+ described_class.new(client, token, :refresh_token => 'abaca',
188
+ :expires_in => 600,
189
+ :param_name => 'o_param')
190
+ end
191
+
192
+ it 'returns a refresh token with appropriate values carried over' do
193
+ refreshed = access.refresh!
194
+ expect(access.client).to eq(refreshed.client)
195
+ expect(access.options[:param_name]).to eq(refreshed.options[:param_name])
196
+ end
197
+
198
+ context 'with a nil refresh_token in the response' do
199
+ let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => nil) }
200
+
201
+ it 'copies the refresh_token from the original token' do
202
+ refreshed = access.refresh!
203
+
204
+ expect(refreshed.refresh_token).to eq(access.refresh_token)
205
+ end
206
+ end
207
+ end
208
+
209
+ describe '#to_hash' do
210
+ it 'return a hash equals to the hash used to initialize access token' do
211
+ hash = {:access_token => token, :refresh_token => 'foobar', :expires_at => Time.now.to_i + 200, 'foo' => 'bar'}
212
+ access_token = described_class.from_hash(client, hash.clone)
213
+ expect(access_token.to_hash).to eq(hash)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,84 @@
1
+ describe OAuth2::Authenticator do
2
+ subject do
3
+ described_class.new(client_id, client_secret, mode)
4
+ end
5
+
6
+ let(:client_id) { 'foo' }
7
+ let(:client_secret) { 'bar' }
8
+ let(:mode) { :undefined }
9
+
10
+ it 'raises NotImplementedError for unknown authentication mode' do
11
+ expect { subject.apply({}) }.to raise_error(NotImplementedError)
12
+ end
13
+
14
+ describe '#apply' do
15
+ context 'with parameter-based authentication' do
16
+ let(:mode) { :request_body }
17
+
18
+ it 'adds client_id and client_secret to params' do
19
+ output = subject.apply({})
20
+ expect(output).to eq('client_id' => 'foo', 'client_secret' => 'bar')
21
+ end
22
+
23
+ it 'does not overwrite existing credentials' do
24
+ input = {'client_secret' => 's3cr3t'}
25
+ output = subject.apply(input)
26
+ expect(output).to eq('client_id' => 'foo', 'client_secret' => 's3cr3t')
27
+ end
28
+
29
+ it 'preserves other parameters' do
30
+ input = {'state' => '42', :headers => {'A' => 'b'}}
31
+ output = subject.apply(input)
32
+ expect(output).to eq(
33
+ 'client_id' => 'foo',
34
+ 'client_secret' => 'bar',
35
+ 'state' => '42',
36
+ :headers => {'A' => 'b'}
37
+ )
38
+ end
39
+
40
+ context 'using tls client authentication' do
41
+ let(:mode) { :tls_client_auth }
42
+
43
+ it 'does not add client_secret' do
44
+ output = subject.apply({})
45
+ expect(output).to eq('client_id' => 'foo')
46
+ end
47
+ end
48
+
49
+ context 'using private key jwt authentication' do
50
+ let(:mode) { :private_key_jwt }
51
+
52
+ it 'does not add client_secret or client_id' do
53
+ output = subject.apply({})
54
+ expect(output).to eq({})
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'with Basic authentication' do
60
+ let(:mode) { :basic_auth }
61
+ let(:header) { 'Basic ' + Base64.encode64("#{client_id}:#{client_secret}").delete("\n") }
62
+
63
+ it 'encodes credentials in headers' do
64
+ output = subject.apply({})
65
+ expect(output).to eq(:headers => {'Authorization' => header})
66
+ end
67
+
68
+ it 'does not overwrite existing credentials' do
69
+ input = {:headers => {'Authorization' => 'Bearer abc123'}}
70
+ output = subject.apply(input)
71
+ expect(output).to eq(:headers => {'Authorization' => 'Bearer abc123'})
72
+ end
73
+
74
+ it 'does not overwrite existing params or headers' do
75
+ input = {'state' => '42', :headers => {'A' => 'b'}}
76
+ output = subject.apply(input)
77
+ expect(output).to eq(
78
+ 'state' => '42',
79
+ :headers => {'A' => 'b', 'Authorization' => header}
80
+ )
81
+ end
82
+ end
83
+ end
84
+ end
@@ -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