oauth2 1.4.4 → 1.4.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/CODE_OF_CONDUCT.md +105 -46
  4. data/LICENSE +1 -1
  5. data/README.md +277 -112
  6. data/lib/oauth2/access_token.rb +8 -7
  7. data/lib/oauth2/authenticator.rb +1 -1
  8. data/lib/oauth2/client.rb +64 -21
  9. data/lib/oauth2/error.rb +1 -1
  10. data/lib/oauth2/mac_token.rb +16 -10
  11. data/lib/oauth2/response.rb +5 -3
  12. data/lib/oauth2/strategy/assertion.rb +3 -3
  13. data/lib/oauth2/strategy/password.rb +2 -2
  14. data/lib/oauth2/version.rb +9 -3
  15. data/spec/helper.rb +30 -0
  16. data/spec/oauth2/access_token_spec.rb +216 -0
  17. data/spec/oauth2/authenticator_spec.rb +84 -0
  18. data/spec/oauth2/client_spec.rb +530 -0
  19. data/spec/oauth2/mac_token_spec.rb +120 -0
  20. data/spec/oauth2/response_spec.rb +90 -0
  21. data/spec/oauth2/strategy/assertion_spec.rb +59 -0
  22. data/spec/oauth2/strategy/auth_code_spec.rb +107 -0
  23. data/spec/oauth2/strategy/base_spec.rb +5 -0
  24. data/spec/oauth2/strategy/client_credentials_spec.rb +69 -0
  25. data/spec/oauth2/strategy/implicit_spec.rb +26 -0
  26. data/spec/oauth2/strategy/password_spec.rb +56 -0
  27. data/spec/oauth2/version_spec.rb +23 -0
  28. metadata +41 -57
  29. data/.document +0 -5
  30. data/.gitignore +0 -19
  31. data/.jrubyrc +0 -1
  32. data/.rspec +0 -2
  33. data/.rubocop.yml +0 -80
  34. data/.rubocop_rspec.yml +0 -26
  35. data/.rubocop_todo.yml +0 -15
  36. data/.ruby-version +0 -1
  37. data/.travis.yml +0 -87
  38. data/CONTRIBUTING.md +0 -18
  39. data/Gemfile +0 -40
  40. data/Rakefile +0 -45
  41. data/gemfiles/jruby_1.7.gemfile +0 -11
  42. data/gemfiles/jruby_9.0.gemfile +0 -7
  43. data/gemfiles/jruby_9.1.gemfile +0 -3
  44. data/gemfiles/jruby_9.2.gemfile +0 -3
  45. data/gemfiles/jruby_head.gemfile +0 -3
  46. data/gemfiles/ruby_1.9.gemfile +0 -11
  47. data/gemfiles/ruby_2.0.gemfile +0 -6
  48. data/gemfiles/ruby_2.1.gemfile +0 -6
  49. data/gemfiles/ruby_2.2.gemfile +0 -3
  50. data/gemfiles/ruby_2.3.gemfile +0 -3
  51. data/gemfiles/ruby_2.4.gemfile +0 -3
  52. data/gemfiles/ruby_2.5.gemfile +0 -3
  53. data/gemfiles/ruby_2.6.gemfile +0 -9
  54. data/gemfiles/ruby_2.7.gemfile +0 -9
  55. data/gemfiles/ruby_head.gemfile +0 -9
  56. data/gemfiles/truffleruby.gemfile +0 -3
  57. data/oauth2.gemspec +0 -52
data/lib/oauth2/client.rb CHANGED
@@ -4,6 +4,8 @@ require 'logger'
4
4
  module OAuth2
5
5
  # The OAuth2::Client class
6
6
  class Client # rubocop:disable Metrics/ClassLength
7
+ RESERVED_PARAM_KEYS = %w[headers parse].freeze
8
+
7
9
  attr_reader :id, :secret, :site
8
10
  attr_accessor :options
9
11
  attr_writer :connection
@@ -23,8 +25,8 @@ module OAuth2
23
25
  # @option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
24
26
  # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
25
27
  # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
26
- # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error
27
- # on responses with 400+ status codes
28
+ # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
29
+ # @option opts [Proc] :extract_access_token proc that extracts the access token from the response
28
30
  # @yield [builder] The Faraday connection builder
29
31
  def initialize(client_id, client_secret, options = {}, &block)
30
32
  opts = options.dup
@@ -32,14 +34,18 @@ module OAuth2
32
34
  @secret = client_secret
33
35
  @site = opts.delete(:site)
34
36
  ssl = opts.delete(:ssl)
35
- @options = {:authorize_url => '/oauth/authorize',
36
- :token_url => '/oauth/token',
37
- :token_method => :post,
38
- :auth_scheme => :request_body,
39
- :connection_opts => {},
40
- :connection_build => block,
41
- :max_redirects => 5,
42
- :raise_errors => true}.merge(opts)
37
+
38
+ @options = {
39
+ :authorize_url => '/oauth/authorize',
40
+ :token_url => '/oauth/token',
41
+ :token_method => :post,
42
+ :auth_scheme => :request_body,
43
+ :connection_opts => {},
44
+ :connection_build => block,
45
+ :max_redirects => 5,
46
+ :raise_errors => true,
47
+ :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN,
48
+ }.merge(opts)
43
49
  @options[:connection_opts][:ssl] = ssl if ssl
44
50
  end
45
51
 
@@ -53,15 +59,12 @@ module OAuth2
53
59
 
54
60
  # The Faraday connection object
55
61
  def connection
56
- @connection ||= begin
57
- conn = Faraday.new(site, options[:connection_opts])
58
- if options[:connection_build]
59
- conn.build do |b|
60
- options[:connection_build].call(b)
62
+ @connection ||=
63
+ Faraday.new(site, options[:connection_opts]) do |builder|
64
+ if options[:connection_build]
65
+ options[:connection_build].call(builder)
61
66
  end
62
67
  end
63
- conn
64
- end
65
68
  end
66
69
 
67
70
  # The authorize endpoint URL of the OAuth2 provider
@@ -91,7 +94,7 @@ module OAuth2
91
94
  # code response for this request. Will default to client option
92
95
  # @option opts [Symbol] :parse @see Response::initialize
93
96
  # @yield [req] The Faraday request
94
- def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, MethodLength, Metrics/AbcSize
97
+ def request(verb, url, opts = {}) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
95
98
  connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true'
96
99
 
97
100
  url = connection.build_url(url).to_s
@@ -107,6 +110,7 @@ module OAuth2
107
110
  opts[:redirect_count] ||= 0
108
111
  opts[:redirect_count] += 1
109
112
  return response if opts[:redirect_count] > options[:max_redirects]
113
+
110
114
  if response.status == 303
111
115
  verb = :get
112
116
  opts.delete(:body)
@@ -118,6 +122,7 @@ module OAuth2
118
122
  when 400..599
119
123
  error = Error.new(response)
120
124
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
125
+
121
126
  response.error = error
122
127
  response
123
128
  else
@@ -132,7 +137,16 @@ module OAuth2
132
137
  # @param [Hash] access token options, to pass to the AccessToken object
133
138
  # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
134
139
  # @return [AccessToken] the initialized AccessToken
135
- def get_token(params, access_token_opts = {}, access_token_class = AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
140
+ def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
141
+ params = params.map do |key, value|
142
+ if RESERVED_PARAM_KEYS.include?(key)
143
+ [key.to_sym, value]
144
+ else
145
+ [key, value]
146
+ end
147
+ end
148
+ params = Hash[params]
149
+
136
150
  params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
137
151
  opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
138
152
  headers = params.delete(:headers) || {}
@@ -145,11 +159,18 @@ module OAuth2
145
159
  end
146
160
  opts[:headers].merge!(headers)
147
161
  response = request(options[:token_method], token_url, opts)
148
- if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed['access_token'])
162
+
163
+ access_token = begin
164
+ build_access_token(response, access_token_opts, extract_access_token)
165
+ rescue StandardError
166
+ nil
167
+ end
168
+
169
+ if options[:raise_errors] && !access_token
149
170
  error = Error.new(response)
150
171
  raise(error)
151
172
  end
152
- access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
173
+ access_token
153
174
  end
154
175
 
155
176
  # The Authorization Code strategy
@@ -207,5 +228,27 @@ module OAuth2
207
228
  {}
208
229
  end
209
230
  end
231
+
232
+ DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
233
+ token = hash.delete('access_token') || hash.delete(:access_token)
234
+ token && AccessToken.new(client, token, hash)
235
+ end
236
+
237
+ private
238
+
239
+ def build_access_token(response, access_token_opts, extract_access_token)
240
+ parsed_response = response.parsed.dup
241
+ return unless parsed_response.is_a?(Hash)
242
+
243
+ hash = parsed_response.merge(access_token_opts)
244
+
245
+ # Provide backwards compatibility for old AcessToken.form_hash pattern
246
+ # Should be deprecated in 2.x
247
+ if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
248
+ extract_access_token.from_hash(self, hash)
249
+ else
250
+ extract_access_token.call(self, hash)
251
+ end
252
+ end
210
253
  end
211
254
  end
data/lib/oauth2/error.rb CHANGED
@@ -23,7 +23,7 @@ module OAuth2
23
23
  def error_message(response_body, opts = {})
24
24
  message = []
25
25
 
26
- opts[:error_description] && message << opts[:error_description]
26
+ opts[:error_description] && (message << opts[:error_description])
27
27
 
28
28
  error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
29
29
  script_encoding = opts[:error_description].encoding
@@ -95,16 +95,22 @@ module OAuth2
95
95
  #
96
96
  # @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256')
97
97
  def algorithm=(alg)
98
- @algorithm = begin
99
- case alg.to_s
100
- when 'hmac-sha-1'
101
- OpenSSL::Digest::SHA1.new
102
- when 'hmac-sha-256'
103
- OpenSSL::Digest::SHA256.new
104
- else
105
- raise(ArgumentError, 'Unsupported algorithm')
106
- end
107
- end
98
+ @algorithm = case alg.to_s
99
+ when 'hmac-sha-1'
100
+ begin
101
+ OpenSSL::Digest('SHA1').new
102
+ rescue StandardError
103
+ OpenSSL::Digest.new('SHA1')
104
+ end
105
+ when 'hmac-sha-256'
106
+ begin
107
+ OpenSSL::Digest('SHA256').new
108
+ rescue StandardError
109
+ OpenSSL::Digest.new('SHA256')
110
+ end
111
+ else
112
+ raise(ArgumentError, 'Unsupported algorithm')
113
+ end
108
114
  end
109
115
 
110
116
  private
@@ -11,9 +11,9 @@ module OAuth2
11
11
  # Procs that, when called, will parse a response body according
12
12
  # to the specified format.
13
13
  @@parsers = {
14
- :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable RescueModifier
14
+ :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
15
15
  :query => lambda { |body| Rack::Utils.parse_query(body) },
16
- :text => lambda { |body| body },
16
+ :text => lambda { |body| body },
17
17
  }
18
18
 
19
19
  # Content type assignments for various potential HTTP content types.
@@ -68,6 +68,7 @@ module OAuth2
68
68
  # application/json Content-Type response bodies
69
69
  def parsed
70
70
  return nil unless @@parsers.key?(parser)
71
+
71
72
  @parsed ||= @@parsers[parser].call(body)
72
73
  end
73
74
 
@@ -79,11 +80,12 @@ module OAuth2
79
80
  # Determines the parser that will be used to supply the content of #parsed
80
81
  def parser
81
82
  return options[:parse].to_sym if @@parsers.key?(options[:parse])
83
+
82
84
  @@content_types[content_type]
83
85
  end
84
86
  end
85
87
  end
86
88
 
87
89
  OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
88
- MultiXml.parse(body) rescue body # rubocop:disable RescueModifier
90
+ MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
89
91
  end
@@ -50,10 +50,10 @@ module OAuth2
50
50
  def build_request(params)
51
51
  assertion = build_assertion(params)
52
52
  {
53
- :grant_type => 'assertion',
53
+ :grant_type => 'assertion',
54
54
  :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
55
- :assertion => assertion,
56
- :scope => params[:scope],
55
+ :assertion => assertion,
56
+ :scope => params[:scope],
57
57
  }
58
58
  end
59
59
 
@@ -18,8 +18,8 @@ module OAuth2
18
18
  # @param [Hash] params additional params
19
19
  def get_token(username, password, params = {}, opts = {})
20
20
  params = {'grant_type' => 'password',
21
- 'username' => username,
22
- 'password' => password}.merge(params)
21
+ 'username' => username,
22
+ 'password' => password}.merge(params)
23
23
  @client.get_token(params, opts)
24
24
  end
25
25
  end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
4
  module Version
5
+ VERSION = to_s
6
+
3
7
  module_function
4
8
 
5
9
  # The major version
@@ -20,12 +24,12 @@ module OAuth2
20
24
  #
21
25
  # @return [Integer]
22
26
  def patch
23
- 4
27
+ 8
24
28
  end
25
29
 
26
30
  # The pre-release version, if any
27
31
  #
28
- # @return [Integer, NilClass]
32
+ # @return [String, NilClass]
29
33
  def pre
30
34
  nil
31
35
  end
@@ -53,7 +57,9 @@ module OAuth2
53
57
  #
54
58
  # @return [String]
55
59
  def to_s
56
- to_a.join('.')
60
+ v = [major, minor, patch].compact.join('.')
61
+ v += "-#{pre}" if pre
62
+ v
57
63
  end
58
64
  end
59
65
  end
data/spec/helper.rb ADDED
@@ -0,0 +1,30 @@
1
+ DEBUG = ENV['DEBUG'] == 'true'
2
+
3
+ ruby_version = Gem::Version.new(RUBY_VERSION)
4
+ minimum_version = ->(version) { ruby_version >= Gem::Version.new(version) && RUBY_ENGINE == 'ruby' }
5
+ coverage = minimum_version.call('2.7')
6
+ debug = minimum_version.call('2.5')
7
+
8
+ require 'simplecov' if coverage
9
+ require 'byebug' if DEBUG && debug
10
+
11
+ require 'oauth2'
12
+ require 'addressable/uri'
13
+ require 'rspec'
14
+ require 'rspec/stubbed_env'
15
+ require 'rspec/pending_for'
16
+ require 'silent_stream'
17
+
18
+ RSpec.configure do |config|
19
+ config.expect_with :rspec do |c|
20
+ c.syntax = :expect
21
+ end
22
+ end
23
+
24
+ Faraday.default_adapter = :test
25
+
26
+ RSpec.configure do |conf|
27
+ conf.include SilentStream
28
+ end
29
+
30
+ 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