oauth2 2.0.9 → 2.0.22
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 +3 -0
- data/CHANGELOG.md +833 -182
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +262 -34
- data/FUNDING.md +74 -0
- data/LICENSE.md +110 -0
- data/README.md +923 -351
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +11 -16
- data/certs/pboling.pem +27 -0
- data/lib/oauth2/access_token.rb +233 -38
- data/lib/oauth2/auth_sanitizer.rb +36 -0
- data/lib/oauth2/authenticator.rb +43 -9
- data/lib/oauth2/client.rb +353 -97
- data/lib/oauth2/error.rb +37 -17
- data/lib/oauth2/filtered_attributes.rb +10 -0
- data/lib/oauth2/response.rb +87 -49
- data/lib/oauth2/strategy/assertion.rb +10 -7
- data/lib/oauth2/strategy/auth_code.rb +13 -3
- data/lib/oauth2/strategy/client_credentials.rb +2 -2
- data/lib/oauth2/strategy/implicit.rb +11 -3
- data/lib/oauth2/strategy/password.rb +14 -4
- data/lib/oauth2/version.rb +2 -1
- data/lib/oauth2.rb +86 -23
- data/sig/oauth2/access_token.rbs +25 -0
- data/sig/oauth2/authenticator.rbs +22 -0
- data/sig/oauth2/client.rbs +52 -0
- data/sig/oauth2/error.rbs +8 -0
- data/sig/oauth2/filtered_attributes.rbs +11 -0
- data/sig/oauth2/response.rbs +18 -0
- data/sig/oauth2/sanitized_logger.rbs +32 -0
- data/sig/oauth2/strategy.rbs +34 -0
- data/sig/oauth2/thing_filter.rbs +10 -0
- data/sig/oauth2/version.rbs +6 -0
- data/sig/oauth2.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +270 -76
- metadata.gz.sig +0 -0
- data/LICENSE +0 -22
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 snaky_hash_klass 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. When debug logging is enabled, sensitive values are filtered using {OAuth2::AUTH_SANITIZER::SanitizedLogger} initialized from `OAuth2.config[:filtered_label]` and the key names in `OAuth2.config[:filtered_debug_keys]`.
|
|
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,111 @@ 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
|
-
|
|
129
|
-
|
|
162
|
+
current_location = response.response.env.url
|
|
163
|
+
full_location = resolve_redirect_location(current_location, location)
|
|
164
|
+
request(verb, full_location, sanitize_redirect_options(req_opts, current_location, full_location))
|
|
130
165
|
else
|
|
131
166
|
error = Error.new(response)
|
|
132
|
-
raise(error, "Got #{
|
|
167
|
+
raise(error, "Got #{status} status code, but no Location header was present")
|
|
133
168
|
end
|
|
134
169
|
when 200..299, 300..399
|
|
135
|
-
# on non-redirecting 3xx statuses,
|
|
170
|
+
# on non-redirecting 3xx statuses, return the response
|
|
136
171
|
response
|
|
137
172
|
when 400..599
|
|
138
|
-
|
|
139
|
-
|
|
173
|
+
if req_opts.fetch(:raise_errors, options[:raise_errors])
|
|
174
|
+
error = Error.new(response)
|
|
175
|
+
raise(error)
|
|
176
|
+
end
|
|
140
177
|
|
|
141
178
|
response
|
|
142
179
|
else
|
|
143
180
|
error = Error.new(response)
|
|
144
|
-
raise(error, "Unhandled status code value of #{
|
|
181
|
+
raise(error, "Unhandled status code value of #{status}")
|
|
145
182
|
end
|
|
146
183
|
end
|
|
147
184
|
|
|
148
|
-
#
|
|
185
|
+
# Retrieves an access token from the token endpoint using the specified parameters
|
|
186
|
+
#
|
|
187
|
+
# @param [Hash] params a Hash of params for the token endpoint
|
|
188
|
+
# * params can include a 'headers' key with a Hash of request headers
|
|
189
|
+
# * params can include a 'parse' key with the Symbol name of response parsing strategy (default: :automatic)
|
|
190
|
+
# * params can include a 'snaky' key to control snake_case conversion (default: false)
|
|
191
|
+
# @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
|
|
192
|
+
# @param [Proc] extract_access_token (deprecated) a proc that can extract the access token from the response
|
|
193
|
+
#
|
|
194
|
+
# @yield [opts] The block is passed the options being used to make the request
|
|
195
|
+
# @yieldparam [Hash] opts options being passed to the http library
|
|
149
196
|
#
|
|
150
|
-
# @
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
# @
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
# @
|
|
197
|
+
# @return [AccessToken, nil] the initialized AccessToken instance, or nil if token extraction fails
|
|
198
|
+
# and raise_errors is false
|
|
199
|
+
#
|
|
200
|
+
# @note The extract_access_token parameter is deprecated and will be removed in oauth2 v3.
|
|
201
|
+
# Use access_token_class on initialization instead.
|
|
202
|
+
#
|
|
203
|
+
# @example
|
|
204
|
+
# client.get_token(
|
|
205
|
+
# 'grant_type' => 'authorization_code',
|
|
206
|
+
# 'code' => 'auth_code_value',
|
|
207
|
+
# 'headers' => {'Authorization' => 'Basic ...'}
|
|
208
|
+
# )
|
|
157
209
|
def get_token(params, access_token_opts = {}, extract_access_token = nil, &block)
|
|
158
|
-
warn(
|
|
210
|
+
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
211
|
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)
|
|
212
|
+
req_opts = params_to_req_opts(params)
|
|
213
|
+
response = request(http_method, token_url, req_opts, &block)
|
|
184
214
|
|
|
185
215
|
# In v1.4.x, the deprecated extract_access_token option retrieves the token from the response.
|
|
186
216
|
# We preserve this behavior here, but a custom access_token_class that implements #from_hash
|
|
@@ -192,8 +222,52 @@ module OAuth2
|
|
|
192
222
|
end
|
|
193
223
|
end
|
|
194
224
|
|
|
225
|
+
# Makes a request to revoke a token at the authorization server
|
|
226
|
+
#
|
|
227
|
+
# @param [String] token The token to be revoked
|
|
228
|
+
# @param [String, nil] token_type_hint A hint about the type of the token being revoked (e.g., 'access_token' or 'refresh_token')
|
|
229
|
+
# @param [Hash] params additional parameters for the token revocation
|
|
230
|
+
# @option params [Symbol] :parse (:automatic) parsing strategy for the response
|
|
231
|
+
# @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
|
|
232
|
+
# @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
|
|
233
|
+
# @option params [Hash] :headers Additional request headers
|
|
234
|
+
#
|
|
235
|
+
# @yield [req] The block is passed the request being made, allowing customization
|
|
236
|
+
# @yieldparam [Faraday::Request] req The request object that can be modified
|
|
237
|
+
#
|
|
238
|
+
# @return [OAuth2::Response] OAuth2::Response instance
|
|
239
|
+
#
|
|
240
|
+
# @api public
|
|
241
|
+
#
|
|
242
|
+
# @note If the token passed to the request
|
|
243
|
+
# is an access token, the server MAY revoke the respective refresh
|
|
244
|
+
# token as well.
|
|
245
|
+
# @note If the token passed to the request
|
|
246
|
+
# is a refresh token and the authorization server supports the
|
|
247
|
+
# revocation of access tokens, then the authorization server SHOULD
|
|
248
|
+
# also invalidate all access tokens based on the same authorization
|
|
249
|
+
# grant
|
|
250
|
+
# @note If the server responds with HTTP status code 503, your code must
|
|
251
|
+
# assume the token still exists and may retry after a reasonable delay.
|
|
252
|
+
# The server may include a "Retry-After" header in the response to
|
|
253
|
+
# indicate how long the service is expected to be unavailable to the
|
|
254
|
+
# requesting client.
|
|
255
|
+
#
|
|
256
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7009
|
|
257
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
|
|
258
|
+
def revoke_token(token, token_type_hint = nil, params = {}, &block)
|
|
259
|
+
params[:token_method] ||= :post_with_query_string
|
|
260
|
+
params[:token] = token
|
|
261
|
+
params[:token_type_hint] = token_type_hint if token_type_hint
|
|
262
|
+
|
|
263
|
+
req_opts = params_to_req_opts(params)
|
|
264
|
+
|
|
265
|
+
request(http_method, revoke_url, req_opts, &block)
|
|
266
|
+
end
|
|
267
|
+
|
|
195
268
|
# The HTTP Method of the request
|
|
196
|
-
#
|
|
269
|
+
#
|
|
270
|
+
# @return [Symbol] HTTP verb, one of [:get, :post, :put, :delete]
|
|
197
271
|
def http_method
|
|
198
272
|
http_meth = options[:token_method].to_sym
|
|
199
273
|
return :post if http_meth == :post_with_query_string
|
|
@@ -229,6 +303,15 @@ module OAuth2
|
|
|
229
303
|
@client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
|
|
230
304
|
end
|
|
231
305
|
|
|
306
|
+
# The Assertion strategy
|
|
307
|
+
#
|
|
308
|
+
# This allows for assertion-based authentication where an identity provider
|
|
309
|
+
# asserts the identity of the user or client application seeking access.
|
|
310
|
+
#
|
|
311
|
+
# @see http://datatracker.ietf.org/doc/html/rfc7521
|
|
312
|
+
# @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-assertions-01#section-4.1
|
|
313
|
+
#
|
|
314
|
+
# @return [OAuth2::Strategy::Assertion] the initialized Assertion strategy
|
|
232
315
|
def assertion
|
|
233
316
|
@assertion ||= OAuth2::Strategy::Assertion.new(self)
|
|
234
317
|
end
|
|
@@ -239,7 +322,10 @@ module OAuth2
|
|
|
239
322
|
# requesting authorization. If it is provided at authorization time it MUST
|
|
240
323
|
# also be provided with the token exchange request.
|
|
241
324
|
#
|
|
242
|
-
#
|
|
325
|
+
# OAuth 2.1 note: Authorization Servers must compare redirect URIs using exact string matching.
|
|
326
|
+
# This client simply forwards the configured redirect_uri; the exact-match validation happens server-side.
|
|
327
|
+
#
|
|
328
|
+
# Providing :redirect_uri to the OAuth2::Client instantiation will take
|
|
243
329
|
# care of managing this.
|
|
244
330
|
#
|
|
245
331
|
# @api semipublic
|
|
@@ -248,10 +334,12 @@ module OAuth2
|
|
|
248
334
|
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
|
249
335
|
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
|
|
250
336
|
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
|
|
337
|
+
# @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
|
|
338
|
+
#
|
|
251
339
|
# @return [Hash] the params to add to a request or URL
|
|
252
340
|
def redirection_params
|
|
253
341
|
if options[:redirect_uri]
|
|
254
|
-
{
|
|
342
|
+
{"redirect_uri" => options[:redirect_uri]}
|
|
255
343
|
else
|
|
256
344
|
{}
|
|
257
345
|
end
|
|
@@ -259,6 +347,63 @@ module OAuth2
|
|
|
259
347
|
|
|
260
348
|
private
|
|
261
349
|
|
|
350
|
+
# Processes request parameters and transforms them into request options
|
|
351
|
+
#
|
|
352
|
+
# @param [Hash] params the request parameters to process
|
|
353
|
+
# @option params [Symbol] :parse (:automatic) parsing strategy for the response
|
|
354
|
+
# @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
|
|
355
|
+
# @option params [Class] :snaky_hash_klass (SnakyHash::StringKeyed) class to use for snake_case hash conversion
|
|
356
|
+
# @option params [Symbol] :token_method (:post) HTTP method to use for token request
|
|
357
|
+
# @option params [Hash] :headers Additional HTTP headers for the request
|
|
358
|
+
#
|
|
359
|
+
# @return [Hash] the processed request options
|
|
360
|
+
#
|
|
361
|
+
# @api private
|
|
362
|
+
def params_to_req_opts(params)
|
|
363
|
+
parse, snaky, snaky_hash_klass, token_method, params, headers = parse_snaky_params_headers(params)
|
|
364
|
+
req_opts = {
|
|
365
|
+
raise_errors: options[:raise_errors],
|
|
366
|
+
token_method: token_method || options[:token_method],
|
|
367
|
+
parse: parse,
|
|
368
|
+
snaky: snaky,
|
|
369
|
+
snaky_hash_klass: snaky_hash_klass,
|
|
370
|
+
}
|
|
371
|
+
if req_opts[:token_method] == :post
|
|
372
|
+
# NOTE: If proliferation of request types continues, we should implement a parser solution for Request,
|
|
373
|
+
# just like we have with Response.
|
|
374
|
+
req_opts[:body] = if headers["Content-Type"] == "application/json"
|
|
375
|
+
params.to_json
|
|
376
|
+
else
|
|
377
|
+
params
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
req_opts[:headers] = {"Content-Type" => "application/x-www-form-urlencoded"}
|
|
381
|
+
else
|
|
382
|
+
req_opts[:params] = params
|
|
383
|
+
req_opts[:headers] = {}
|
|
384
|
+
end
|
|
385
|
+
req_opts[:headers].merge!(headers)
|
|
386
|
+
req_opts
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Processes and transforms parameters for OAuth requests
|
|
390
|
+
#
|
|
391
|
+
# @param [Hash] params the input parameters to process
|
|
392
|
+
# @option params [Symbol] :parse (:automatic) parsing strategy for the response
|
|
393
|
+
# @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
|
|
394
|
+
# @option params [Class] :snaky_hash_klass (SnakyHash::StringKeyed) class to use for snake_case hash conversion
|
|
395
|
+
# @option params [Symbol] :token_method overrides the default token method for this request
|
|
396
|
+
# @option params [Hash] :headers HTTP headers for the request
|
|
397
|
+
#
|
|
398
|
+
# @return [Array<(Symbol, Boolean, Class, Symbol, Hash, Hash)>] Returns an array containing:
|
|
399
|
+
# - parse strategy (Symbol)
|
|
400
|
+
# - snaky flag for response key transformation (Boolean)
|
|
401
|
+
# - hash class for snake_case conversion (Class)
|
|
402
|
+
# - token method override (Symbol, nil)
|
|
403
|
+
# - processed parameters (Hash)
|
|
404
|
+
# - HTTP headers (Hash)
|
|
405
|
+
#
|
|
406
|
+
# @api private
|
|
262
407
|
def parse_snaky_params_headers(params)
|
|
263
408
|
params = params.map do |key, value|
|
|
264
409
|
if RESERVED_PARAM_KEYS.include?(key)
|
|
@@ -269,18 +414,44 @@ module OAuth2
|
|
|
269
414
|
end.to_h
|
|
270
415
|
parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
|
|
271
416
|
snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
|
|
417
|
+
snaky_hash_klass = params.key?(:snaky_hash_klass) ? params.delete(:snaky_hash_klass) : Response::DEFAULT_OPTIONS[:snaky_hash_klass]
|
|
418
|
+
token_method = params.delete(:token_method) if params.key?(:token_method)
|
|
272
419
|
params = authenticator.apply(params)
|
|
273
|
-
# authenticator may add :headers, and we
|
|
420
|
+
# authenticator may add :headers, and we separate them from params here
|
|
274
421
|
headers = params.delete(:headers) || {}
|
|
275
|
-
[parse, snaky, params, headers]
|
|
422
|
+
[parse, snaky, snaky_hash_klass, token_method, params, headers]
|
|
276
423
|
end
|
|
277
424
|
|
|
425
|
+
# Executes an HTTP request with error handling and response processing
|
|
426
|
+
#
|
|
427
|
+
# @param [Symbol] verb the HTTP method to use (:get, :post, :put, :delete)
|
|
428
|
+
# @param [String] url the URL for the request
|
|
429
|
+
# @param [Hash] opts the request options
|
|
430
|
+
# @option opts [Hash] :body the request body
|
|
431
|
+
# @option opts [Hash] :headers the request headers
|
|
432
|
+
# @option opts [Hash] :params the query parameters to append to the URL
|
|
433
|
+
# @option opts [Symbol, nil] :parse (:automatic) parsing strategy for the response
|
|
434
|
+
# @option opts [Boolean] :snaky (true) whether to convert response keys to snake_case
|
|
435
|
+
#
|
|
436
|
+
# @yield [req] gives access to the request object before sending
|
|
437
|
+
# @yieldparam [Faraday::Request] req the request object that can be modified
|
|
438
|
+
#
|
|
439
|
+
# @return [OAuth2::Response] the response wrapped in an OAuth2::Response object
|
|
440
|
+
#
|
|
441
|
+
# @raise [OAuth2::ConnectionError] when there's a network error
|
|
442
|
+
# @raise [OAuth2::TimeoutError] when the request times out
|
|
443
|
+
#
|
|
444
|
+
# @api private
|
|
278
445
|
def execute_request(verb, url, opts = {})
|
|
279
446
|
url = connection.build_url(url).to_s
|
|
447
|
+
# See: Hash#partition https://bugs.ruby-lang.org/issues/16252
|
|
448
|
+
req_opts, oauth_opts = opts.
|
|
449
|
+
partition { |k, _v| RESERVED_REQ_KEYS.include?(k.to_s) }.
|
|
450
|
+
map(&:to_h)
|
|
280
451
|
|
|
281
452
|
begin
|
|
282
|
-
response = connection.run_request(verb, url,
|
|
283
|
-
req.params.update(
|
|
453
|
+
response = connection.run_request(verb, url, req_opts[:body], req_opts[:headers]) do |req|
|
|
454
|
+
req.params.update(req_opts[:params]) if req_opts[:params]
|
|
284
455
|
yield(req) if block_given?
|
|
285
456
|
end
|
|
286
457
|
rescue Faraday::ConnectionFailed => e
|
|
@@ -289,12 +460,42 @@ module OAuth2
|
|
|
289
460
|
raise TimeoutError, e
|
|
290
461
|
end
|
|
291
462
|
|
|
292
|
-
parse =
|
|
293
|
-
snaky =
|
|
463
|
+
parse = oauth_opts.key?(:parse) ? oauth_opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
|
|
464
|
+
snaky = oauth_opts.key?(:snaky) ? oauth_opts.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
|
|
294
465
|
|
|
295
466
|
Response.new(response, parse: parse, snaky: snaky)
|
|
296
467
|
end
|
|
297
468
|
|
|
469
|
+
def resolve_redirect_location(current_location, location)
|
|
470
|
+
safe_location =
|
|
471
|
+
if location.respond_to?(:start_with?) && location.start_with?("//")
|
|
472
|
+
"./#{location}"
|
|
473
|
+
else
|
|
474
|
+
location
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
current_location.merge(safe_location)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def sanitize_redirect_options(req_opts, current_location, next_location)
|
|
481
|
+
return req_opts unless cross_origin_redirect?(current_location, next_location)
|
|
482
|
+
|
|
483
|
+
headers = req_opts[:headers]
|
|
484
|
+
return req_opts unless headers && headers.any? { |key, _value| key.to_s.casecmp("Authorization").zero? }
|
|
485
|
+
|
|
486
|
+
safe_opts = req_opts.dup
|
|
487
|
+
safe_headers = headers.dup
|
|
488
|
+
safe_headers.delete_if { |key, _value| key.to_s.casecmp("Authorization").zero? }
|
|
489
|
+
safe_opts[:headers] = safe_headers
|
|
490
|
+
safe_opts
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def cross_origin_redirect?(current_location, next_location)
|
|
494
|
+
current_location.scheme != next_location.scheme ||
|
|
495
|
+
current_location.host != next_location.host ||
|
|
496
|
+
current_location.port != next_location.port
|
|
497
|
+
end
|
|
498
|
+
|
|
298
499
|
# Returns the authenticator object
|
|
299
500
|
#
|
|
300
501
|
# @return [Authenticator] the initialized Authenticator
|
|
@@ -302,6 +503,20 @@ module OAuth2
|
|
|
302
503
|
Authenticator.new(id, secret, options[:auth_scheme])
|
|
303
504
|
end
|
|
304
505
|
|
|
506
|
+
# Parses the OAuth response and builds an access token using legacy extraction method
|
|
507
|
+
#
|
|
508
|
+
# @deprecated Use {#parse_response} instead
|
|
509
|
+
#
|
|
510
|
+
# @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
|
|
511
|
+
# @param [Hash] access_token_opts options to pass to the AccessToken initialization
|
|
512
|
+
# @param [Proc] extract_access_token proc to extract the access token from response
|
|
513
|
+
#
|
|
514
|
+
# @return [AccessToken, nil] the initialized AccessToken if successful, nil if extraction fails
|
|
515
|
+
# and raise_errors option is false
|
|
516
|
+
#
|
|
517
|
+
# @raise [OAuth2::Error] if response indicates an error and raise_errors option is true
|
|
518
|
+
#
|
|
519
|
+
# @api private
|
|
305
520
|
def parse_response_legacy(response, access_token_opts, extract_access_token)
|
|
306
521
|
access_token = build_access_token_legacy(response, access_token_opts, extract_access_token)
|
|
307
522
|
|
|
@@ -315,6 +530,16 @@ module OAuth2
|
|
|
315
530
|
nil
|
|
316
531
|
end
|
|
317
532
|
|
|
533
|
+
# Parses the OAuth response and builds an access token using the configured access token class
|
|
534
|
+
#
|
|
535
|
+
# @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
|
|
536
|
+
# @param [Hash] access_token_opts options to pass to the AccessToken initialization
|
|
537
|
+
#
|
|
538
|
+
# @return [AccessToken] the initialized AccessToken instance
|
|
539
|
+
#
|
|
540
|
+
# @raise [OAuth2::Error] if the response is empty/invalid and the raise_errors option is true
|
|
541
|
+
#
|
|
542
|
+
# @api private
|
|
318
543
|
def parse_response(response, access_token_opts)
|
|
319
544
|
access_token_class = options[:access_token_class]
|
|
320
545
|
data = response.parsed
|
|
@@ -329,26 +554,57 @@ module OAuth2
|
|
|
329
554
|
build_access_token(response, access_token_opts, access_token_class)
|
|
330
555
|
end
|
|
331
556
|
|
|
332
|
-
#
|
|
557
|
+
# Creates an access token instance from response data using the specified token class
|
|
558
|
+
#
|
|
559
|
+
# @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
|
|
560
|
+
# @param [Hash] access_token_opts additional options to pass to the AccessToken initialization
|
|
561
|
+
# @param [Class] access_token_class the class that should be used to create access token instances
|
|
333
562
|
#
|
|
334
|
-
# @return [AccessToken]
|
|
563
|
+
# @return [AccessToken] an initialized AccessToken instance with response data
|
|
564
|
+
#
|
|
565
|
+
# @note If the access token class responds to response=, the full response object will be set
|
|
566
|
+
#
|
|
567
|
+
# @api private
|
|
335
568
|
def build_access_token(response, access_token_opts, access_token_class)
|
|
336
569
|
access_token_class.from_hash(self, response.parsed.merge(access_token_opts)).tap do |access_token|
|
|
337
570
|
access_token.response = response if access_token.respond_to?(:response=)
|
|
338
571
|
end
|
|
339
572
|
end
|
|
340
573
|
|
|
341
|
-
# Builds
|
|
574
|
+
# Builds an access token using a legacy extraction proc
|
|
575
|
+
#
|
|
576
|
+
# @deprecated Use {#build_access_token} instead
|
|
342
577
|
#
|
|
343
|
-
# @
|
|
578
|
+
# @param [OAuth2::Response] response the OAuth2::Response from the token endpoint
|
|
579
|
+
# @param [Hash] access_token_opts additional options to pass to the access token extraction
|
|
580
|
+
# @param [Proc] extract_access_token a proc that takes client and token hash as arguments
|
|
581
|
+
# and returns an access token instance
|
|
582
|
+
#
|
|
583
|
+
# @return [AccessToken, nil] the access token instance if extraction succeeds,
|
|
584
|
+
# nil if any error occurs during extraction
|
|
585
|
+
#
|
|
586
|
+
# @api private
|
|
344
587
|
def build_access_token_legacy(response, access_token_opts, extract_access_token)
|
|
345
588
|
extract_access_token.call(self, response.parsed.merge(access_token_opts))
|
|
346
|
-
rescue
|
|
589
|
+
rescue
|
|
590
|
+
# An error will be raised by the called if nil is returned and options[:raise_errors] is truthy, so this rescue is but temporary.
|
|
591
|
+
# Unfortunately, it does hide the real error, but this is deprecated legacy code,
|
|
592
|
+
# and this was effectively the long-standing pre-existing behavior, so there is little point in changing it.
|
|
347
593
|
nil
|
|
348
594
|
end
|
|
349
595
|
|
|
350
596
|
def oauth_debug_logging(builder)
|
|
351
|
-
|
|
597
|
+
if OAuth2::OAUTH_DEBUG
|
|
598
|
+
builder.response(
|
|
599
|
+
:logger,
|
|
600
|
+
OAuth2::AUTH_SANITIZER::SanitizedLogger.new(
|
|
601
|
+
options[:logger],
|
|
602
|
+
filtered_keys: OAuth2.config[:filtered_debug_keys],
|
|
603
|
+
label: OAuth2.config[:filtered_label]
|
|
604
|
+
),
|
|
605
|
+
bodies: true
|
|
606
|
+
)
|
|
607
|
+
end
|
|
352
608
|
end
|
|
353
609
|
end
|
|
354
610
|
end
|