oauth2 2.0.9 → 2.0.10

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
@@ -1,7 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'faraday'
4
- require 'logger'
3
+ require "faraday"
4
+ require "logger"
5
+
6
+ # :nocov: since coverage tracking only runs on the builds with Faraday v2
7
+ # We do run builds on Faraday v0 (and v1!), so this code is actually covered!
8
+ # This is the only nocov in the whole project!
9
+ if Faraday::Utils.respond_to?(:default_space_encoding)
10
+ # This setting doesn't exist in faraday 0.x
11
+ Faraday::Utils.default_space_encoding = "%20"
12
+ end
13
+ # :nocov:
5
14
 
6
15
  module OAuth2
7
16
  ConnectionError = Class.new(Faraday::ConnectionFailed)
@@ -9,31 +18,34 @@ module OAuth2
9
18
 
10
19
  # The OAuth2::Client class
11
20
  class Client # rubocop:disable Metrics/ClassLength
12
- RESERVED_PARAM_KEYS = %w[body headers params parse snaky].freeze
21
+ RESERVED_REQ_KEYS = %w[body headers params redirect_count].freeze
22
+ RESERVED_PARAM_KEYS = (RESERVED_REQ_KEYS + %w[parse snaky token_method]).freeze
23
+
24
+ include FilteredAttributes
13
25
 
14
26
  attr_reader :id, :secret, :site
15
27
  attr_accessor :options
16
28
  attr_writer :connection
29
+ filtered_attributes :secret
17
30
 
18
- # Instantiate a new OAuth 2.0 client using the
19
- # Client ID and Client Secret registered to your
20
- # application.
31
+ # Initializes a new OAuth2::Client instance using the Client ID and Client Secret registered to your application.
21
32
  #
22
33
  # @param [String] client_id the client_id value
23
34
  # @param [String] client_secret the client_secret value
24
- # @param [Hash] options the options to create the client with
35
+ # @param [Hash] options the options to configure the client
25
36
  # @option options [String] :site the OAuth2 provider site host
26
- # @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
27
37
  # @option options [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
38
+ # @option options [String] :revoke_url ('/oauth/revoke') absolute or relative URL path to the Revoke endpoint
28
39
  # @option options [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
29
40
  # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get, :post, :post_with_query_string)
30
- # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
31
- # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
32
- # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
33
- # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
34
- # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
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+
41
+ # @option options [Symbol] :auth_scheme (:basic_auth) the authentication scheme (:basic_auth, :request_body, :tls_client_auth, :private_key_jwt)
42
+ # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday
43
+ # @option options [Boolean] :raise_errors (true) whether to raise an OAuth2::Error on responses with 400+ status codes
44
+ # @option options [Integer] :max_redirects (5) maximum number of redirects to follow
45
+ # @option options [Logger] :logger (::Logger.new($stdout)) Logger instance for HTTP request/response output; requires OAUTH_DEBUG to be true
46
+ # @option options [Class] :access_token_class (AccessToken) class to use for access tokens; you can subclass OAuth2::AccessToken, @version 2.0+
47
+ # @option options [Hash] :ssl SSL options for Faraday
48
+ #
37
49
  # @yield [builder] The Faraday connection builder
38
50
  def initialize(client_id, client_secret, options = {}, &block)
39
51
  opts = options.dup
@@ -41,10 +53,11 @@ module OAuth2
41
53
  @secret = client_secret
42
54
  @site = opts.delete(:site)
43
55
  ssl = opts.delete(:ssl)
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]
56
+ 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
57
  @options = {
46
- authorize_url: 'oauth/authorize',
47
- token_url: 'oauth/token',
58
+ authorize_url: "oauth/authorize",
59
+ revoke_url: "oauth/revoke",
60
+ token_url: "oauth/token",
48
61
  token_method: :post,
49
62
  auth_scheme: :basic_auth,
50
63
  connection_opts: {},
@@ -59,13 +72,16 @@ module OAuth2
59
72
 
60
73
  # Set the site host
61
74
  #
62
- # @param value [String] the OAuth2 provider site host
75
+ # @param [String] value the OAuth2 provider site host
76
+ # @return [String] the site host value
63
77
  def site=(value)
64
78
  @connection = nil
65
79
  @site = value
66
80
  end
67
81
 
68
82
  # The Faraday connection object
83
+ #
84
+ # @return [Faraday::Connection] the initialized Faraday connection
69
85
  def connection
70
86
  @connection ||=
71
87
  Faraday.new(site, options[:connection_opts]) do |builder|
@@ -73,8 +89,8 @@ module OAuth2
73
89
  if options[:connection_build]
74
90
  options[:connection_build].call(builder)
75
91
  else
76
- builder.request :url_encoded # form-encode POST params
77
- builder.adapter Faraday.default_adapter # make requests with Net::HTTP
92
+ builder.request(:url_encoded) # form-encode POST params
93
+ builder.adapter(Faraday.default_adapter) # make requests with Net::HTTP
78
94
  end
79
95
  end
80
96
  end
@@ -82,6 +98,7 @@ module OAuth2
82
98
  # The authorize endpoint URL of the OAuth2 provider
83
99
  #
84
100
  # @param [Hash] params additional query parameters
101
+ # @return [String] the constructed authorize URL
85
102
  def authorize_url(params = {})
86
103
  params = (params || {}).merge(redirection_params)
87
104
  connection.build_url(options[:authorize_url], params).to_s
@@ -89,98 +106,110 @@ module OAuth2
89
106
 
90
107
  # The token endpoint URL of the OAuth2 provider
91
108
  #
92
- # @param [Hash] params additional query parameters
109
+ # @param [Hash, nil] params additional query parameters
110
+ # @return [String] the constructed token URL
93
111
  def token_url(params = nil)
94
112
  connection.build_url(options[:token_url], params).to_s
95
113
  end
96
114
 
115
+ # The revoke endpoint URL of the OAuth2 provider
116
+ #
117
+ # @param [Hash, nil] params additional query parameters
118
+ # @return [String] the constructed revoke URL
119
+ def revoke_url(params = nil)
120
+ connection.build_url(options[:revoke_url], params).to_s
121
+ end
122
+
97
123
  # Makes a request relative to the specified site root.
124
+ #
98
125
  # Updated HTTP 1.1 specification (IETF RFC 7231) relaxed the original constraint (IETF RFC 2616),
99
126
  # allowing the use of relative URLs in Location headers.
127
+ #
100
128
  # @see https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2
101
129
  #
102
- # @param [Symbol] verb one of :get, :post, :put, :delete
130
+ # @param [Symbol] verb one of [:get, :post, :put, :delete]
103
131
  # @param [String] url URL path of request
104
- # @param [Hash] opts the options to make the request with
105
- # @option opts [Hash] :params additional query parameters for the URL of the request
106
- # @option opts [Hash, String] :body the body of the request
107
- # @option opts [Hash] :headers http request headers
108
- # @option opts [Boolean] :raise_errors whether or not to raise an OAuth2::Error on 400+ status
109
- # code response for this request. Will default to client option
110
- # @option opts [Symbol] :parse @see Response::initialize
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)
115
-
116
- case response.status
132
+ # @param [Hash] req_opts the options to make the request with
133
+ # @option req_opts [Hash] :params additional query parameters for the URL of the request
134
+ # @option req_opts [Hash, String] :body the body of the request
135
+ # @option req_opts [Hash] :headers http request headers
136
+ # @option req_opts [Boolean] :raise_errors whether to raise an OAuth2::Error on 400+ status
137
+ # code response for this request. Overrides the client instance setting.
138
+ # @option req_opts [Symbol] :parse @see Response::initialize
139
+ # @option req_opts [Boolean] :snaky (true) @see Response::initialize
140
+ #
141
+ # @yield [req] The block is passed the request being made, allowing customization
142
+ # @yieldparam [Faraday::Request] req The request object that can be modified
143
+ # @see Faraday::Connection#run_request
144
+ #
145
+ # @return [OAuth2::Response] the response from the request
146
+ def request(verb, url, req_opts = {}, &block)
147
+ response = execute_request(verb, url, req_opts, &block)
148
+ status = response.status
149
+
150
+ case status
117
151
  when 301, 302, 303, 307
118
- opts[:redirect_count] ||= 0
119
- opts[:redirect_count] += 1
120
- return response if opts[:redirect_count] > options[:max_redirects]
152
+ req_opts[:redirect_count] ||= 0
153
+ req_opts[:redirect_count] += 1
154
+ return response if req_opts[:redirect_count] > options[:max_redirects]
121
155
 
122
- if response.status == 303
156
+ if status == 303
123
157
  verb = :get
124
- opts.delete(:body)
158
+ req_opts.delete(:body)
125
159
  end
126
- location = response.headers['location']
160
+ location = response.headers["location"]
127
161
  if location
128
162
  full_location = response.response.env.url.merge(location)
129
- request(verb, full_location, opts)
163
+ request(verb, full_location, req_opts)
130
164
  else
131
165
  error = Error.new(response)
132
- raise(error, "Got #{response.status} status code, but no Location header was present")
166
+ raise(error, "Got #{status} status code, but no Location header was present")
133
167
  end
134
168
  when 200..299, 300..399
135
- # on non-redirecting 3xx statuses, just return the response
169
+ # on non-redirecting 3xx statuses, return the response
136
170
  response
137
171
  when 400..599
138
- error = Error.new(response)
139
- raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
172
+ if req_opts.fetch(:raise_errors, options[:raise_errors])
173
+ error = Error.new(response)
174
+ raise(error)
175
+ end
140
176
 
141
177
  response
142
178
  else
143
179
  error = Error.new(response)
144
- raise(error, "Unhandled status code value of #{response.status}")
180
+ raise(error, "Unhandled status code value of #{status}")
145
181
  end
146
182
  end
147
183
 
148
- # Initializes an AccessToken by making a request to the token endpoint
184
+ # Retrieves an access token from the token endpoint using the specified parameters
185
+ #
186
+ # @param [Hash] params a Hash of params for the token endpoint
187
+ # * params can include a 'headers' key with a Hash of request headers
188
+ # * params can include a 'parse' key with the Symbol name of response parsing strategy (default: :automatic)
189
+ # * params can include a 'snaky' key to control snake_case conversion (default: false)
190
+ # @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
191
+ # @param [Proc] extract_access_token (deprecated) a proc that can extract the access token from the response
192
+ #
193
+ # @yield [opts] The block is passed the options being used to make the request
194
+ # @yieldparam [Hash] opts options being passed to the http library
195
+ #
196
+ # @return [AccessToken, nil] the initialized AccessToken instance, or nil if token extraction fails
197
+ # and raise_errors is false
198
+ #
199
+ # @note The extract_access_token parameter is deprecated and will be removed in oauth2 v3.
200
+ # Use access_token_class on initialization instead.
149
201
  #
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
153
- # @param access_token_opts [Hash] access token options, to pass to the AccessToken object
154
- # @param extract_access_token [Proc] proc that extracts the access token from the response (DEPRECATED)
155
- # @yield [req] @see Faraday::Connection#run_request
156
- # @return [AccessToken] the initialized AccessToken
202
+ # @example
203
+ # client.get_token(
204
+ # 'grant_type' => 'authorization_code',
205
+ # 'code' => 'auth_code_value',
206
+ # 'headers' => {'Authorization' => 'Basic ...'}
207
+ # )
157
208
  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
209
+ 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
210
  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
- }
167
- if options[:token_method] == :post
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'}
178
- else
179
- request_opts[:params] = params
180
- request_opts[:headers] = {}
181
- end
182
- request_opts[:headers].merge!(headers)
183
- response = request(http_method, token_url, request_opts, &block)
211
+ req_opts = params_to_req_opts(params)
212
+ response = request(http_method, token_url, req_opts, &block)
184
213
 
185
214
  # In v1.4.x, the deprecated extract_access_token option retrieves the token from the response.
186
215
  # We preserve this behavior here, but a custom access_token_class that implements #from_hash
@@ -192,8 +221,52 @@ module OAuth2
192
221
  end
193
222
  end
194
223
 
224
+ # Makes a request to revoke a token at the authorization server
225
+ #
226
+ # @param [String] token The token to be revoked
227
+ # @param [String, nil] token_type_hint A hint about the type of the token being revoked (e.g., 'access_token' or 'refresh_token')
228
+ # @param [Hash] params additional parameters for the token revocation
229
+ # @option params [Symbol] :parse (:automatic) parsing strategy for the response
230
+ # @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
231
+ # @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
232
+ # @option params [Hash] :headers Additional request headers
233
+ #
234
+ # @yield [req] The block is passed the request being made, allowing customization
235
+ # @yieldparam [Faraday::Request] req The request object that can be modified
236
+ #
237
+ # @return [OAuth2::Response] OAuth2::Response instance
238
+ #
239
+ # @api public
240
+ #
241
+ # @note If the token passed to the request
242
+ # is an access token, the server MAY revoke the respective refresh
243
+ # token as well.
244
+ # @note If the token passed to the request
245
+ # is a refresh token and the authorization server supports the
246
+ # revocation of access tokens, then the authorization server SHOULD
247
+ # also invalidate all access tokens based on the same authorization
248
+ # grant
249
+ # @note If the server responds with HTTP status code 503, your code must
250
+ # assume the token still exists and may retry after a reasonable delay.
251
+ # The server may include a "Retry-After" header in the response to
252
+ # indicate how long the service is expected to be unavailable to the
253
+ # requesting client.
254
+ #
255
+ # @see https://datatracker.ietf.org/doc/html/rfc7009
256
+ # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
257
+ def revoke_token(token, token_type_hint = nil, params = {}, &block)
258
+ params[:token_method] ||= :post_with_query_string
259
+ req_opts = params_to_req_opts(params)
260
+ req_opts[:params] ||= {}
261
+ req_opts[:params][:token] = token
262
+ req_opts[:params][:token_type_hint] = token_type_hint if token_type_hint
263
+
264
+ request(http_method, revoke_url, req_opts, &block)
265
+ end
266
+
195
267
  # The HTTP Method of the request
196
- # @return [Symbol] HTTP verb, one of :get, :post, :put, :delete
268
+ #
269
+ # @return [Symbol] HTTP verb, one of [:get, :post, :put, :delete]
197
270
  def http_method
198
271
  http_meth = options[:token_method].to_sym
199
272
  return :post if http_meth == :post_with_query_string
@@ -229,6 +302,15 @@ module OAuth2
229
302
  @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
230
303
  end
231
304
 
305
+ # The Assertion strategy
306
+ #
307
+ # This allows for assertion-based authentication where an identity provider
308
+ # asserts the identity of the user or client application seeking access.
309
+ #
310
+ # @see http://datatracker.ietf.org/doc/html/rfc7521
311
+ # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-assertions-01#section-4.1
312
+ #
313
+ # @return [OAuth2::Strategy::Assertion] the initialized Assertion strategy
232
314
  def assertion
233
315
  @assertion ||= OAuth2::Strategy::Assertion.new(self)
234
316
  end
@@ -239,7 +321,7 @@ module OAuth2
239
321
  # requesting authorization. If it is provided at authorization time it MUST
240
322
  # also be provided with the token exchange request.
241
323
  #
242
- # Providing the :redirect_uri to the OAuth2::Client instantiation will take
324
+ # Providing :redirect_uri to the OAuth2::Client instantiation will take
243
325
  # care of managing this.
244
326
  #
245
327
  # @api semipublic
@@ -248,10 +330,11 @@ module OAuth2
248
330
  # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
249
331
  # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
250
332
  # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
333
+ #
251
334
  # @return [Hash] the params to add to a request or URL
252
335
  def redirection_params
253
336
  if options[:redirect_uri]
254
- {'redirect_uri' => options[:redirect_uri]}
337
+ {"redirect_uri" => options[:redirect_uri]}
255
338
  else
256
339
  {}
257
340
  end
@@ -259,6 +342,48 @@ module OAuth2
259
342
 
260
343
  private
261
344
 
345
+ # A generic token request options parser
346
+ def params_to_req_opts(params)
347
+ parse, snaky, token_method, params, headers = parse_snaky_params_headers(params)
348
+ req_opts = {
349
+ raise_errors: options[:raise_errors],
350
+ token_method: token_method || options[:token_method],
351
+ parse: parse,
352
+ snaky: snaky,
353
+ }
354
+ if req_opts[:token_method] == :post
355
+ # NOTE: If proliferation of request types continues, we should implement a parser solution for Request,
356
+ # just like we have with Response.
357
+ req_opts[:body] = if headers["Content-Type"] == "application/json"
358
+ params.to_json
359
+ else
360
+ params
361
+ end
362
+
363
+ req_opts[:headers] = {"Content-Type" => "application/x-www-form-urlencoded"}
364
+ else
365
+ req_opts[:params] = params
366
+ req_opts[:headers] = {}
367
+ end
368
+ req_opts[:headers].merge!(headers)
369
+ req_opts
370
+ end
371
+
372
+ # Processes and transforms the input parameters for OAuth requests
373
+ #
374
+ # @param [Hash] params the input parameters to process
375
+ # @option params [Symbol, nil] :parse (:automatic) parsing strategy for the response
376
+ # @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
377
+ # @option params [Hash] :headers HTTP headers for the request
378
+ #
379
+ # @return [Array<(Symbol, Boolean, Hash, Hash)>] Returns an array containing:
380
+ # - [Symbol, nil] parse strategy
381
+ # - [Boolean] snaky flag for response key transformation
382
+ # - [Symbol, nil] token_method overrides options[:token_method] for a request
383
+ # - [Hash] processed parameters
384
+ # - [Hash] HTTP headers
385
+ #
386
+ # @api private
262
387
  def parse_snaky_params_headers(params)
263
388
  params = params.map do |key, value|
264
389
  if RESERVED_PARAM_KEYS.include?(key)
@@ -269,18 +394,43 @@ module OAuth2
269
394
  end.to_h
270
395
  parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
271
396
  snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
397
+ token_method = params.delete(:token_method) if params.key?(:token_method)
272
398
  params = authenticator.apply(params)
273
- # authenticator may add :headers, and we remove them here
399
+ # authenticator may add :headers, and we separate them from params here
274
400
  headers = params.delete(:headers) || {}
275
- [parse, snaky, params, headers]
401
+ [parse, snaky, token_method, params, headers]
276
402
  end
277
403
 
404
+ # Executes an HTTP request with error handling and response processing
405
+ #
406
+ # @param [Symbol] verb the HTTP method to use (:get, :post, :put, :delete)
407
+ # @param [String] url the URL for the request
408
+ # @param [Hash] opts the request options
409
+ # @option opts [Hash] :body the request body
410
+ # @option opts [Hash] :headers the request headers
411
+ # @option opts [Hash] :params the query parameters to append to the URL
412
+ # @option opts [Symbol, nil] :parse (:automatic) parsing strategy for the response
413
+ # @option opts [Boolean] :snaky (true) whether to convert response keys to snake_case
414
+ #
415
+ # @yield [req] gives access to the request object before sending
416
+ # @yieldparam [Faraday::Request] req the request object that can be modified
417
+ #
418
+ # @return [OAuth2::Response] the response wrapped in an OAuth2::Response object
419
+ #
420
+ # @raise [OAuth2::ConnectionError] when there's a network error
421
+ # @raise [OAuth2::TimeoutError] when the request times out
422
+ #
423
+ # @api private
278
424
  def execute_request(verb, url, opts = {})
279
425
  url = connection.build_url(url).to_s
426
+ # See: Hash#partition https://bugs.ruby-lang.org/issues/16252
427
+ req_opts, oauth_opts = opts.
428
+ partition { |k, _v| RESERVED_REQ_KEYS.include?(k.to_s) }.
429
+ map { |p| Hash[p] }
280
430
 
281
431
  begin
282
- response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
283
- req.params.update(opts[:params]) if opts[:params]
432
+ response = connection.run_request(verb, url, req_opts[:body], req_opts[:headers]) do |req|
433
+ req.params.update(req_opts[:params]) if req_opts[:params]
284
434
  yield(req) if block_given?
285
435
  end
286
436
  rescue Faraday::ConnectionFailed => e
@@ -289,8 +439,8 @@ module OAuth2
289
439
  raise TimeoutError, e
290
440
  end
291
441
 
292
- parse = opts.key?(:parse) ? opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
293
- snaky = opts.key?(:snaky) ? opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
442
+ parse = oauth_opts.key?(:parse) ? oauth_opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
443
+ snaky = oauth_opts.key?(:snaky) ? oauth_opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
294
444
 
295
445
  Response.new(response, parse: parse, snaky: snaky)
296
446
  end
@@ -302,6 +452,20 @@ module OAuth2
302
452
  Authenticator.new(id, secret, options[:auth_scheme])
303
453
  end
304
454
 
455
+ # Parses the OAuth response and builds an access token using legacy extraction method
456
+ #
457
+ # @deprecated Use {#parse_response} instead
458
+ #
459
+ # @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
460
+ # @param [Hash] access_token_opts options to pass to the AccessToken initialization
461
+ # @param [Proc] extract_access_token proc to extract the access token from response
462
+ #
463
+ # @return [AccessToken, nil] the initialized AccessToken if successful, nil if extraction fails
464
+ # and raise_errors option is false
465
+ #
466
+ # @raise [OAuth2::Error] if response indicates an error and raise_errors option is true
467
+ #
468
+ # @api private
305
469
  def parse_response_legacy(response, access_token_opts, extract_access_token)
306
470
  access_token = build_access_token_legacy(response, access_token_opts, extract_access_token)
307
471
 
@@ -315,6 +479,16 @@ module OAuth2
315
479
  nil
316
480
  end
317
481
 
482
+ # Parses the OAuth response and builds an access token using the configured access token class
483
+ #
484
+ # @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
485
+ # @param [Hash] access_token_opts options to pass to the AccessToken initialization
486
+ #
487
+ # @return [AccessToken] the initialized AccessToken instance
488
+ #
489
+ # @raise [OAuth2::Error] if the response is empty/invalid and the raise_errors option is true
490
+ #
491
+ # @api private
318
492
  def parse_response(response, access_token_opts)
319
493
  access_token_class = options[:access_token_class]
320
494
  data = response.parsed
@@ -329,26 +503,47 @@ module OAuth2
329
503
  build_access_token(response, access_token_opts, access_token_class)
330
504
  end
331
505
 
332
- # Builds the access token from the response of the HTTP call
506
+ # Creates an access token instance from response data using the specified token class
507
+ #
508
+ # @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
509
+ # @param [Hash] access_token_opts additional options to pass to the AccessToken initialization
510
+ # @param [Class] access_token_class the class that should be used to create access token instances
511
+ #
512
+ # @return [AccessToken] an initialized AccessToken instance with response data
333
513
  #
334
- # @return [AccessToken] the initialized AccessToken
514
+ # @note If the access token class responds to response=, the full response object will be set
515
+ #
516
+ # @api private
335
517
  def build_access_token(response, access_token_opts, access_token_class)
336
518
  access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
337
519
  access_token.response = response if access_token.respond_to?(:response=)
338
520
  end
339
521
  end
340
522
 
341
- # Builds the access token from the response of the HTTP call with legacy extract_access_token
523
+ # Builds an access token using a legacy extraction proc
524
+ #
525
+ # @deprecated Use {#build_access_token} instead
526
+ #
527
+ # @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
528
+ # @param [Hash] access_token_opts additional options to pass to the access token extraction
529
+ # @param [Proc] extract_access_token a proc that takes client and token hash as arguments
530
+ # and returns an access token instance
531
+ #
532
+ # @return [AccessToken, nil] the access token instance if extraction succeeds,
533
+ # nil if any error occurs during extraction
342
534
  #
343
- # @return [AccessToken] the initialized AccessToken
535
+ # @api private
344
536
  def build_access_token_legacy(response, access_token_opts, extract_access_token)
345
537
  extract_access_token.call(self, response.parsed.merge(access_token_opts))
346
- rescue StandardError
538
+ rescue
539
+ # An error will be raised by the called if nil is returned and options[:raise_errors] is truthy, so this rescue is but temporary.
540
+ # Unfortunately, it does hide the real error, but this is deprecated legacy code,
541
+ # and this was effectively the long-standing pre-existing behavior, so there is little point in changing it.
347
542
  nil
348
543
  end
349
544
 
350
545
  def oauth_debug_logging(builder)
351
- builder.response :logger, options[:logger], bodies: true if ENV['OAUTH_DEBUG'] == 'true'
546
+ builder.response(:logger, options[:logger], bodies: true) if OAuth2::OAUTH_DEBUG
352
547
  end
353
548
  end
354
549
  end
data/lib/oauth2/error.rb CHANGED
@@ -11,18 +11,18 @@ module OAuth2
11
11
  @response = response
12
12
  if response.respond_to?(:parsed)
13
13
  if response.parsed.is_a?(Hash)
14
- @code = response.parsed['error']
15
- @description = response.parsed['error_description']
14
+ @code = response.parsed["error"]
15
+ @description = response.parsed["error_description"]
16
16
  end
17
17
  elsif response.is_a?(Hash)
18
- @code = response['error']
19
- @description = response['error_description']
18
+ @code = response["error"]
19
+ @description = response["error_description"]
20
20
  end
21
21
  @body = if response.respond_to?(:body)
22
- response.body
23
- else
24
- @response
25
- end
22
+ response.body
23
+ else
24
+ @response
25
+ end
26
26
  message_opts = parse_error_description(@code, @description)
27
27
  super(error_message(@body, message_opts))
28
28
  end
@@ -35,11 +35,11 @@ module OAuth2
35
35
  lines << opts[:error_description] if opts[:error_description]
36
36
 
37
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
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
43
 
44
44
  lines << error_string
45
45
 
@@ -49,7 +49,7 @@ module OAuth2
49
49
  def parse_error_description(code, description)
50
50
  return {} unless code || description
51
51
 
52
- error_description = ''
52
+ error_description = ""
53
53
  error_description += "#{code}: " if code
54
54
  error_description += description if description
55
55
 
@@ -0,0 +1,31 @@
1
+ module OAuth2
2
+ module FilteredAttributes
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+ def filtered_attributes(*attributes)
9
+ @filtered_attribute_names = attributes.map(&:to_sym)
10
+ end
11
+
12
+ def filtered_attribute_names
13
+ @filtered_attribute_names || []
14
+ end
15
+ end
16
+
17
+ def inspect
18
+ filtered_attribute_names = self.class.filtered_attribute_names
19
+ return super if filtered_attribute_names.empty?
20
+
21
+ inspected_vars = instance_variables.map do |var|
22
+ if filtered_attribute_names.any? { |filtered_var| var.to_s.include?(filtered_var.to_s) }
23
+ "#{var}=[FILTERED]"
24
+ else
25
+ "#{var}=#{instance_variable_get(var).inspect}"
26
+ end
27
+ end
28
+ "#<#{self.class}:#{object_id} #{inspected_vars.join(", ")}>"
29
+ end
30
+ end
31
+ end