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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +301 -168
- data/CODE_OF_CONDUCT.md +0 -0
- data/CONTRIBUTING.md +126 -31
- data/{LICENSE → LICENSE.txt} +1 -1
- data/README.md +619 -245
- data/SECURITY.md +2 -2
- data/lib/oauth2/access_token.rb +186 -32
- data/lib/oauth2/authenticator.rb +9 -6
- data/lib/oauth2/client.rb +291 -96
- data/lib/oauth2/error.rb +14 -14
- data/lib/oauth2/filtered_attributes.rb +31 -0
- data/lib/oauth2/response.rb +17 -13
- data/lib/oauth2/strategy/assertion.rb +4 -4
- data/lib/oauth2/strategy/auth_code.rb +3 -3
- data/lib/oauth2/strategy/base.rb +0 -0
- data/lib/oauth2/strategy/client_credentials.rb +2 -2
- data/lib/oauth2/strategy/implicit.rb +3 -3
- data/lib/oauth2/strategy/password.rb +6 -4
- data/lib/oauth2/version.rb +1 -1
- data/lib/oauth2.rb +23 -18
- data.tar.gz.sig +0 -0
- metadata +188 -67
- metadata.gz.sig +0 -0
data/lib/oauth2/client.rb
CHANGED
@@ -1,7 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
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
|
-
|
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
|
-
#
|
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
|
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)
|
31
|
-
# @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday
|
32
|
-
# @option options [
|
33
|
-
# @option options [
|
34
|
-
# @option options [Logger] :logger (::Logger.new($stdout))
|
35
|
-
# @option options [
|
36
|
-
# @option options [
|
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(
|
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:
|
47
|
-
|
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
|
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
|
77
|
-
builder.adapter
|
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]
|
105
|
-
# @option
|
106
|
-
# @option
|
107
|
-
# @option
|
108
|
-
# @option
|
109
|
-
# code response for this request.
|
110
|
-
# @option
|
111
|
-
# @option
|
112
|
-
#
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
119
|
-
|
120
|
-
return response if
|
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
|
156
|
+
if status == 303
|
123
157
|
verb = :get
|
124
|
-
|
158
|
+
req_opts.delete(:body)
|
125
159
|
end
|
126
|
-
location = response.headers[
|
160
|
+
location = response.headers["location"]
|
127
161
|
if location
|
128
162
|
full_location = response.response.env.url.merge(location)
|
129
|
-
request(verb, full_location,
|
163
|
+
request(verb, full_location, req_opts)
|
130
164
|
else
|
131
165
|
error = Error.new(response)
|
132
|
-
raise(error, "Got #{
|
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,
|
169
|
+
# on non-redirecting 3xx statuses, return the response
|
136
170
|
response
|
137
171
|
when 400..599
|
138
|
-
|
139
|
-
|
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 #{
|
180
|
+
raise(error, "Unhandled status code value of #{status}")
|
145
181
|
end
|
146
182
|
end
|
147
183
|
|
148
|
-
#
|
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
|
-
# @
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
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(
|
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
|
-
|
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
|
-
#
|
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
|
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
|
-
{
|
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
|
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,
|
283
|
-
req.params.update(
|
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 =
|
293
|
-
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
|
-
#
|
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
|
-
# @
|
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
|
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
|
-
# @
|
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
|
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
|
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[
|
15
|
-
@description = response.parsed[
|
14
|
+
@code = response.parsed["error"]
|
15
|
+
@description = response.parsed["error_description"]
|
16
16
|
end
|
17
17
|
elsif response.is_a?(Hash)
|
18
|
-
@code = response[
|
19
|
-
@description = response[
|
18
|
+
@code = response["error"]
|
19
|
+
@description = response["error_description"]
|
20
20
|
end
|
21
21
|
@body = if response.respond_to?(:body)
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|