oauth2 1.4.10 → 2.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,40 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OAuth2
4
- class AccessToken
5
- attr_reader :client, :token, :expires_in, :expires_at, :params
6
- attr_accessor :options, :refresh_token
4
+ class AccessToken # rubocop:disable Metrics/ClassLength
5
+ TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
6
+ TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
7
+ TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
8
+
9
+ attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
10
+ attr_accessor :options, :refresh_token, :response
7
11
 
8
- # Should these methods be deprecated?
9
12
  class << self
10
13
  # Initializes an AccessToken from a Hash
11
14
  #
12
- # @param [Client] the OAuth2::Client instance
13
- # @param [Hash] a hash of AccessToken property values
14
- # @return [AccessToken] the initalized AccessToken
15
+ # @param [Client] client the OAuth2::Client instance
16
+ # @param [Hash] hash a hash of AccessToken property values
17
+ # @option hash [String, Symbol] 'access_token', 'id_token', 'token', :access_token, :id_token, or :token the access token
18
+ # @return [AccessToken] the initialized AccessToken
15
19
  def from_hash(client, hash)
16
- hash = hash.dup
17
- new(client, hash.delete('access_token') || hash.delete(:access_token), hash)
20
+ fresh = hash.dup
21
+ supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
22
+ key = supported_keys[0]
23
+ extra_tokens_warning(supported_keys, key)
24
+ token = fresh.delete(key)
25
+ new(client, token, fresh)
18
26
  end
19
27
 
20
28
  # Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
21
29
  #
22
30
  # @param [Client] client the OAuth2::Client instance
23
31
  # @param [String] kvform the application/x-www-form-urlencoded string
24
- # @return [AccessToken] the initalized AccessToken
32
+ # @return [AccessToken] the initialized AccessToken
25
33
  def from_kvform(client, kvform)
26
34
  from_hash(client, Rack::Utils.parse_query(kvform))
27
35
  end
36
+
37
+ private
38
+
39
+ # Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
40
+ def extra_tokens_warning(supported_keys, key)
41
+ return if OAuth2.config.silence_extra_tokens_warning
42
+ return if supported_keys.length <= 1
43
+
44
+ warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
45
+ end
28
46
  end
29
47
 
30
- # Initalize an AccessToken
48
+ # Initialize an AccessToken
31
49
  #
32
50
  # @param [Client] client the OAuth2::Client instance
33
- # @param [String] token the Access Token value
51
+ # @param [String] token the Access Token value (optional, may not be used in refresh flows)
34
52
  # @param [Hash] opts the options to create the Access Token with
35
53
  # @option opts [String] :refresh_token (nil) the refresh_token value
36
54
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
37
55
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
56
+ # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
38
57
  # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
39
58
  # one of :header, :body or :query
40
59
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
@@ -43,17 +62,30 @@ module OAuth2
43
62
  def initialize(client, token, opts = {})
44
63
  @client = client
45
64
  @token = token.to_s
65
+
46
66
  opts = opts.dup
47
- [:refresh_token, :expires_in, :expires_at].each do |arg|
67
+ %i[refresh_token expires_in expires_at expires_latency].each do |arg|
48
68
  instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
49
69
  end
70
+ no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
71
+ if no_tokens
72
+ if @client.options[:raise_errors]
73
+ error = Error.new(opts)
74
+ raise(error)
75
+ else
76
+ warn('OAuth2::AccessToken has no token')
77
+ end
78
+ end
79
+ # @option opts [Fixnum, String] :expires is deprecated
50
80
  @expires_in ||= opts.delete('expires')
51
81
  @expires_in &&= @expires_in.to_i
52
82
  @expires_at &&= convert_expires_at(@expires_at)
83
+ @expires_latency &&= @expires_latency.to_i
53
84
  @expires_at ||= Time.now.to_i + @expires_in if @expires_in
54
- @options = {:mode => opts.delete(:mode) || :header,
55
- :header_format => opts.delete(:header_format) || 'Bearer %s',
56
- :param_name => opts.delete(:param_name) || 'access_token'}
85
+ @expires_at -= @expires_latency if @expires_latency
86
+ @options = {mode: opts.delete(:mode) || :header,
87
+ header_format: opts.delete(:header_format) || 'Bearer %s',
88
+ param_name: opts.delete(:param_name) || 'access_token'}
57
89
  @params = opts
58
90
  end
59
91
 
@@ -75,29 +107,36 @@ module OAuth2
75
107
  #
76
108
  # @return [Boolean]
77
109
  def expired?
78
- expires? && (expires_at < Time.now.to_i)
110
+ expires? && (expires_at <= Time.now.to_i)
79
111
  end
80
112
 
81
113
  # Refreshes the current Access Token
82
114
  #
83
115
  # @return [AccessToken] a new AccessToken
84
116
  # @note options should be carried over to the new AccessToken
85
- def refresh!(params = {})
117
+ def refresh(params = {}, access_token_opts = {})
86
118
  raise('A refresh_token is not available') unless refresh_token
87
119
 
88
120
  params[:grant_type] = 'refresh_token'
89
121
  params[:refresh_token] = refresh_token
90
- new_token = @client.get_token(params)
122
+ new_token = @client.get_token(params, access_token_opts)
91
123
  new_token.options = options
92
- new_token.refresh_token = refresh_token unless new_token.refresh_token
124
+ if new_token.refresh_token
125
+ # Keep it, if there is one
126
+ else
127
+ new_token.refresh_token = refresh_token
128
+ end
93
129
  new_token
94
130
  end
131
+ # A compatibility alias
132
+ # @note does not modify the receiver, so bang is not the default method
133
+ alias refresh! refresh
95
134
 
96
135
  # Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
97
136
  #
98
137
  # @return [Hash] a hash of AccessToken property values
99
138
  def to_hash
100
- params.merge(:access_token => token, :refresh_token => refresh_token, :expires_at => expires_at)
139
+ params.merge(access_token: token, refresh_token: refresh_token, expires_at: expires_at)
101
140
  end
102
141
 
103
142
  # Make a request with the Access Token
@@ -105,7 +144,7 @@ module OAuth2
105
144
  # @param [Symbol] verb the HTTP request method
106
145
  # @param [String] path the HTTP URL path of the request
107
146
  # @param [Hash] opts the options to make the request with
108
- # @see Client#request
147
+ # @see Client#request
109
148
  def request(verb, path, opts = {}, &block)
110
149
  configure_authentication!(opts)
111
150
  @client.request(verb, path, opts, &block)
@@ -166,7 +205,7 @@ module OAuth2
166
205
  if opts[:body].is_a?(Hash)
167
206
  opts[:body][options[:param_name]] = token
168
207
  else
169
- opts[:body] << "&#{options[:param_name]}=#{token}"
208
+ opts[:body] += "&#{options[:param_name]}=#{token}"
170
209
  end
171
210
  # @todo support for multi-part (file uploads)
172
211
  else
@@ -37,7 +37,7 @@ module OAuth2
37
37
  end
38
38
 
39
39
  def self.encode_basic_auth(user, password)
40
- 'Basic ' + Base64.encode64(user + ':' + password).delete("\n")
40
+ "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
41
41
  end
42
42
 
43
43
  private
@@ -45,13 +45,18 @@ module OAuth2
45
45
  # Adds client_id and client_secret request parameters if they are not
46
46
  # already set.
47
47
  def apply_params_auth(params)
48
- {'client_id' => id, 'client_secret' => secret}.merge(params)
48
+ result = {}
49
+ result['client_id'] = id unless id.nil?
50
+ result['client_secret'] = secret unless secret.nil?
51
+ result.merge(params)
49
52
  end
50
53
 
51
54
  # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
52
55
  # we don't want to send the secret
53
56
  def apply_client_id(params)
54
- {'client_id' => id}.merge(params)
57
+ result = {}
58
+ result['client_id'] = id unless id.nil?
59
+ result.merge(params)
55
60
  end
56
61
 
57
62
  # Adds an `Authorization` header with Basic Auth credentials if and only if
@@ -59,7 +64,7 @@ module OAuth2
59
64
  def apply_basic_auth(params)
60
65
  headers = params.fetch(:headers, {})
61
66
  headers = basic_auth_header.merge(headers)
62
- params.merge(:headers => headers)
67
+ params.merge(headers: headers)
63
68
  end
64
69
 
65
70
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
data/lib/oauth2/client.rb CHANGED
@@ -5,9 +5,11 @@ require 'logger'
5
5
 
6
6
  module OAuth2
7
7
  ConnectionError = Class.new(Faraday::ConnectionFailed)
8
+ TimeoutError = Class.new(Faraday::TimeoutError)
9
+
8
10
  # The OAuth2::Client class
9
11
  class Client # rubocop:disable Metrics/ClassLength
10
- RESERVED_PARAM_KEYS = %w[headers parse].freeze
12
+ RESERVED_PARAM_KEYS = %w[body headers params parse snaky].freeze
11
13
 
12
14
  attr_reader :id, :secret, :site
13
15
  attr_accessor :options
@@ -22,15 +24,16 @@ module OAuth2
22
24
  # @param [Hash] options the options to create the client with
23
25
  # @option options [String] :site the OAuth2 provider site host
24
26
  # @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
25
- # @option options [String] :authorize_url ('oauth/authorize') absolute or relative URL path to the Authorization endpoint
26
- # @option options [String] :token_url ('oauth/token') absolute or relative URL path to the Token endpoint
27
- # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
27
+ # @option options [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
28
+ # @option options [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
29
+ # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get, :post, :post_with_query_string)
28
30
  # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
29
31
  # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
30
32
  # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
31
33
  # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
32
34
  # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
33
- # @option options [Proc] (DEPRECATED) :extract_access_token proc that extracts the access token from the response
35
+ # @option options [Proc] :extract_access_token proc that takes the client and the response Hash and extracts the access token from the response (DEPRECATED)
36
+ # @option options [Class] :access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken, @version 2.0+
34
37
  # @yield [builder] The Faraday connection builder
35
38
  def initialize(client_id, client_secret, options = {}, &block)
36
39
  opts = options.dup
@@ -38,16 +41,19 @@ module OAuth2
38
41
  @secret = client_secret
39
42
  @site = opts.delete(:site)
40
43
  ssl = opts.delete(:ssl)
41
- @options = {:authorize_url => 'oauth/authorize',
42
- :token_url => 'oauth/token',
43
- :token_method => :post,
44
- :auth_scheme => :request_body,
45
- :connection_opts => {},
46
- :connection_build => block,
47
- :max_redirects => 5,
48
- :raise_errors => true,
49
- :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN, # DEPRECATED
50
- :logger => ::Logger.new($stdout)}.merge(opts)
44
+ warn('OAuth2::Client#initialize argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class`.') if opts[:extract_access_token]
45
+ @options = {
46
+ authorize_url: 'oauth/authorize',
47
+ token_url: 'oauth/token',
48
+ token_method: :post,
49
+ auth_scheme: :basic_auth,
50
+ connection_opts: {},
51
+ connection_build: block,
52
+ max_redirects: 5,
53
+ raise_errors: true,
54
+ logger: ::Logger.new($stdout),
55
+ access_token_class: AccessToken,
56
+ }.merge(opts)
51
57
  @options[:connection_opts][:ssl] = ssl if ssl
52
58
  end
53
59
 
@@ -89,6 +95,9 @@ module OAuth2
89
95
  end
90
96
 
91
97
  # Makes a request relative to the specified site root.
98
+ # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616),
99
+ # allowing the use of relative URLs in Location headers.
100
+ # @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2
92
101
  #
93
102
  # @param [Symbol] verb one of :get, :post, :put, :delete
94
103
  # @param [String] url URL path of request
@@ -99,20 +108,10 @@ module OAuth2
99
108
  # @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
100
109
  # code response for this request. Will default to client option
101
110
  # @option opts [Symbol] :parse @see Response::initialize
102
- # @yield [req] The Faraday request
103
- def request(verb, url, opts = {}) # rubocop:disable Metrics/AbcSize
104
- url = connection.build_url(url).to_s
105
-
106
- begin
107
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
108
- req.params.update(opts[:params]) if opts[:params]
109
- yield(req) if block_given?
110
- end
111
- rescue Faraday::ConnectionFailed => e
112
- raise ConnectionError, e
113
- end
114
-
115
- response = Response.new(response, :parse => opts[:parse])
111
+ # @option opts [true, false] :snaky (true) @see Response::initialize
112
+ # @yield [req] @see Faraday::Connection#run_request
113
+ def request(verb, url, opts = {}, &block)
114
+ response = execute_request(verb, url, opts, &block)
116
115
 
117
116
  case response.status
118
117
  when 301, 302, 303, 307
@@ -126,7 +125,8 @@ module OAuth2
126
125
  end
127
126
  location = response.headers['location']
128
127
  if location
129
- request(verb, location, opts)
128
+ full_location = response.response.env.url.merge(location)
129
+ request(verb, full_location, opts)
130
130
  else
131
131
  error = Error.new(response)
132
132
  raise(error, "Got #{response.status} status code, but no Location header was present")
@@ -138,7 +138,6 @@ module OAuth2
138
138
  error = Error.new(response)
139
139
  raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
140
140
 
141
- response.error = error
142
141
  response
143
142
  else
144
143
  error = Error.new(response)
@@ -148,53 +147,58 @@ module OAuth2
148
147
 
149
148
  # Initializes an AccessToken by making a request to the token endpoint
150
149
  #
151
- # @param params [Hash] a Hash of params for the token endpoint
150
+ # @param params [Hash] a Hash of params for the token endpoint, except:
151
+ # @option params [Symbol] :parse @see Response#initialize
152
+ # @option params [true, false] :snaky (true) @see Response#initialize
152
153
  # @param access_token_opts [Hash] access token options, to pass to the AccessToken object
153
- # @param access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken
154
+ # @param extract_access_token [Proc] proc that extracts the access token from the response (DEPRECATED)
155
+ # @yield [req] @see Faraday::Connection#run_request
154
156
  # @return [AccessToken] the initialized AccessToken
155
- def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity Metrics/AbcSize, Metrics/MethodLength
156
- params = params.map do |key, value|
157
- if RESERVED_PARAM_KEYS.include?(key)
158
- [key.to_sym, value]
159
- else
160
- [key, value]
161
- end
162
- end
163
- params = Hash[params]
164
-
165
- params = authenticator.apply(params)
166
- opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
167
- headers = params.delete(:headers) || {}
157
+ def get_token(params, access_token_opts = {}, extract_access_token = nil, &block)
158
+ warn('OAuth2::Client#get_token argument `extract_access_token` will be removed in oauth2 v3. Refactor to use `access_token_class` on #initialize.') if extract_access_token
159
+ extract_access_token ||= options[:extract_access_token]
160
+ parse, snaky, params, headers = parse_snaky_params_headers(params)
161
+
162
+ request_opts = {
163
+ raise_errors: options[:raise_errors],
164
+ parse: parse,
165
+ snaky: snaky,
166
+ }
168
167
  if options[:token_method] == :post
169
- opts[:body] = params
170
- opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
168
+
169
+ # NOTE: If proliferation of request types continues we should implement a parser solution for Request,
170
+ # just like we have with Response.
171
+ request_opts[:body] = if headers['Content-Type'] == 'application/json'
172
+ params.to_json
173
+ else
174
+ params
175
+ end
176
+
177
+ request_opts[:headers] = {'Content-Type' => 'application/x-www-form-urlencoded'}
171
178
  else
172
- opts[:params] = params
173
- opts[:headers] = {}
179
+ request_opts[:params] = params
180
+ request_opts[:headers] = {}
174
181
  end
175
- opts[:headers] = opts[:headers].merge(headers)
176
- http_method = options[:token_method]
177
- response = request(http_method, token_url, opts)
178
-
179
- access_token = begin
180
- build_access_token(response, access_token_opts, extract_access_token)
181
- rescue StandardError
182
- nil
182
+ request_opts[:headers].merge!(headers)
183
+ response = request(http_method, token_url, request_opts, &block)
184
+
185
+ # In v1.4.x, the deprecated extract_access_token option retrieves the token from the response.
186
+ # We preserve this behavior here, but a custom access_token_class that implements #from_hash
187
+ # should be used instead.
188
+ if extract_access_token
189
+ parse_response_legacy(response, access_token_opts, extract_access_token)
190
+ else
191
+ parse_response(response, access_token_opts)
183
192
  end
193
+ end
184
194
 
185
- response_contains_token = access_token || (
186
- response.parsed.is_a?(Hash) &&
187
- (response.parsed['access_token'] || response.parsed['id_token'])
188
- )
189
-
190
- if options[:raise_errors] && !response_contains_token
191
- error = Error.new(response)
192
- raise(error)
193
- elsif !response_contains_token
194
- return nil
195
- end
195
+ # The HTTP Method of the request
196
+ # @return [Symbol] HTTP verb, one of :get, :post, :put, :delete
197
+ def http_method
198
+ http_meth = options[:token_method].to_sym
199
+ return :post if http_meth == :post_with_query_string
196
200
 
197
- access_token
201
+ http_meth
198
202
  end
199
203
 
200
204
  # The Authorization Code strategy
@@ -253,12 +257,43 @@ module OAuth2
253
257
  end
254
258
  end
255
259
 
256
- DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
257
- token = hash.delete('access_token') || hash.delete(:access_token)
258
- token && AccessToken.new(client, token, hash)
260
+ private
261
+
262
+ def parse_snaky_params_headers(params)
263
+ params = params.map do |key, value|
264
+ if RESERVED_PARAM_KEYS.include?(key)
265
+ [key.to_sym, value]
266
+ else
267
+ [key, value]
268
+ end
269
+ end.to_h
270
+ parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
271
+ snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
272
+ params = authenticator.apply(params)
273
+ # authenticator may add :headers, and we remove them here
274
+ headers = params.delete(:headers) || {}
275
+ [parse, snaky, params, headers]
259
276
  end
260
277
 
261
- private
278
+ def execute_request(verb, url, opts = {})
279
+ url = connection.build_url(url).to_s
280
+
281
+ begin
282
+ response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
283
+ req.params.update(opts[:params]) if opts[:params]
284
+ yield(req) if block_given?
285
+ end
286
+ rescue Faraday::ConnectionFailed => e
287
+ raise ConnectionError, e
288
+ rescue Faraday::TimeoutError => e
289
+ raise TimeoutError, e
290
+ end
291
+
292
+ parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
293
+ snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
294
+
295
+ Response.new(response, parse: parse, snaky: snaky)
296
+ end
262
297
 
263
298
  # Returns the authenticator object
264
299
  #
@@ -267,26 +302,53 @@ module OAuth2
267
302
  Authenticator.new(id, secret, options[:auth_scheme])
268
303
  end
269
304
 
305
+ def parse_response_legacy(response, access_token_opts, extract_access_token)
306
+ access_token = build_access_token_legacy(response, access_token_opts, extract_access_token)
307
+
308
+ return access_token if access_token
309
+
310
+ if options[:raise_errors]
311
+ error = Error.new(response)
312
+ raise(error)
313
+ end
314
+
315
+ nil
316
+ end
317
+
318
+ def parse_response(response, access_token_opts)
319
+ access_token_class = options[:access_token_class]
320
+ data = response.parsed
321
+
322
+ unless data.is_a?(Hash) && !data.empty?
323
+ return unless options[:raise_errors]
324
+
325
+ error = Error.new(response)
326
+ raise(error)
327
+ end
328
+
329
+ build_access_token(response, access_token_opts, access_token_class)
330
+ end
331
+
270
332
  # Builds the access token from the response of the HTTP call
271
333
  #
272
334
  # @return [AccessToken] the initialized AccessToken
273
- def build_access_token(response, access_token_opts, extract_access_token)
274
- parsed_response = response.parsed.dup
275
- return unless parsed_response.is_a?(Hash)
276
-
277
- hash = parsed_response.merge(access_token_opts)
278
-
279
- # Provide backwards compatibility for old AccessToken.form_hash pattern
280
- # Will be deprecated in 2.x
281
- if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
282
- extract_access_token.from_hash(self, hash)
283
- else
284
- extract_access_token.call(self, hash)
335
+ def build_access_token(response, access_token_opts, access_token_class)
336
+ access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
337
+ access_token.response = response if access_token.respond_to?(:response=)
285
338
  end
286
339
  end
287
340
 
341
+ # Builds the access token from the response of the HTTP call with legacy extract_access_token
342
+ #
343
+ # @return [AccessToken] the initialized AccessToken
344
+ def build_access_token_legacy(response, access_token_opts, extract_access_token)
345
+ extract_access_token.call(self, response.parsed.merge(access_token_opts))
346
+ rescue StandardError
347
+ nil
348
+ end
349
+
288
350
  def oauth_debug_logging(builder)
289
- builder.response :logger, options[:logger], :bodies => true if ENV['OAUTH_DEBUG'] == 'true'
351
+ builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
290
352
  end
291
353
  end
292
354
  end
data/lib/oauth2/error.rb CHANGED
@@ -2,41 +2,58 @@
2
2
 
3
3
  module OAuth2
4
4
  class Error < StandardError
5
- attr_reader :response, :code, :description
5
+ attr_reader :response, :body, :code, :description
6
6
 
7
- # standard error values include:
8
- # :invalid_request, :invalid_client, :invalid_token, :invalid_grant, :unsupported_grant_type, :invalid_scope
7
+ # standard error codes include:
8
+ # 'invalid_request', 'invalid_client', 'invalid_token', 'invalid_grant', 'unsupported_grant_type', 'invalid_scope'
9
+ # response might be a Response object, or the response.parsed hash
9
10
  def initialize(response)
10
- response.error = self
11
11
  @response = response
12
-
13
- if response.parsed.is_a?(Hash)
14
- @code = response.parsed['error']
15
- @description = response.parsed['error_description']
16
- error_description = "#{@code}: #{@description}"
12
+ if response.respond_to?(:parsed)
13
+ if response.parsed.is_a?(Hash)
14
+ @code = response.parsed['error']
15
+ @description = response.parsed['error_description']
16
+ end
17
+ elsif response.is_a?(Hash)
18
+ @code = response['error']
19
+ @description = response['error_description']
17
20
  end
18
-
19
- super(error_message(response.body, :error_description => error_description))
21
+ @body = if response.respond_to?(:body)
22
+ response.body
23
+ else
24
+ @response
25
+ end
26
+ message_opts = parse_error_description(@code, @description)
27
+ super(error_message(@body, message_opts))
20
28
  end
21
29
 
22
- # Makes a error message
23
- # @param [String] response_body response body of request
24
- # @param [String] opts :error_description error description to show first line
30
+ private
31
+
25
32
  def error_message(response_body, opts = {})
26
- message = []
33
+ lines = []
34
+
35
+ lines << opts[:error_description] if opts[:error_description]
27
36
 
28
- opts[:error_description] && (message << opts[:error_description])
37
+ error_string = if response_body.respond_to?(:encode) && opts[:error_description].respond_to?(:encoding)
38
+ script_encoding = opts[:error_description].encoding
39
+ response_body.encode(script_encoding, invalid: :replace, undef: :replace)
40
+ else
41
+ response_body
42
+ end
43
+
44
+ lines << error_string
45
+
46
+ lines.join("\n")
47
+ end
29
48
 
30
- error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
31
- script_encoding = opts[:error_description].encoding
32
- response_body.encode(script_encoding, :invalid => :replace, :undef => :replace)
33
- else
34
- response_body
35
- end
49
+ def parse_error_description(code, description)
50
+ return {} unless code || description
36
51
 
37
- message << error_message
52
+ error_description = ''
53
+ error_description += "#{code}: " if code
54
+ error_description += description if description
38
55
 
39
- message.join("\n")
56
+ {error_description: error_description}
40
57
  end
41
58
  end
42
59
  end