oauth2 1.4.0 → 1.4.8

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.
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,12 +94,13 @@ 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
- url = connection.build_url(url, opts[:params]).to_s
100
+ url = connection.build_url(url).to_s
98
101
 
99
102
  response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
103
+ req.params.update(opts[:params]) if opts[:params]
100
104
  yield(req) if block_given?
101
105
  end
102
106
  response = Response.new(response, :parse => opts[:parse])
@@ -106,6 +110,7 @@ module OAuth2
106
110
  opts[:redirect_count] ||= 0
107
111
  opts[:redirect_count] += 1
108
112
  return response if opts[:redirect_count] > options[:max_redirects]
113
+
109
114
  if response.status == 303
110
115
  verb = :get
111
116
  opts.delete(:body)
@@ -117,6 +122,7 @@ module OAuth2
117
122
  when 400..599
118
123
  error = Error.new(response)
119
124
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
125
+
120
126
  response.error = error
121
127
  response
122
128
  else
@@ -130,8 +136,17 @@ module OAuth2
130
136
  # @param [Hash] params a Hash of params for the token endpoint
131
137
  # @param [Hash] access token options, to pass to the AccessToken object
132
138
  # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
133
- # @return [AccessToken] the initalized AccessToken
134
- def get_token(params, access_token_opts = {}, access_token_class = AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
139
+ # @return [AccessToken] the initialized AccessToken
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
+
135
150
  params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
136
151
  opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
137
152
  headers = params.delete(:headers) || {}
@@ -144,11 +159,18 @@ module OAuth2
144
159
  end
145
160
  opts[:headers].merge!(headers)
146
161
  response = request(options[:token_method], token_url, opts)
147
- 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
148
170
  error = Error.new(response)
149
171
  raise(error)
150
172
  end
151
- access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
173
+ access_token
152
174
  end
153
175
 
154
176
  # The Authorization Code strategy
@@ -206,5 +228,27 @@ module OAuth2
206
228
  {}
207
229
  end
208
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
209
253
  end
210
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,23 +95,29 @@ 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
111
117
 
112
118
  # No-op since we need the verb and path
113
119
  # and the MAC always goes in a header
114
- def token=(_)
120
+ def token=(_noop)
115
121
  end
116
122
 
117
123
  # Base64.strict_encode64 is not available on Ruby 1.8.7
@@ -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
- 0
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