oauth2 1.4.6 → 1.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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