oauth 1.1.0 → 1.1.6
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 +1 -0
- data/CHANGELOG.md +637 -269
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +79 -29
- data/CONTRIBUTING.md +256 -24
- data/FUNDING.md +74 -0
- data/LICENSE.md +71 -0
- data/README.md +615 -287
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +11 -16
- data/certs/pboling.pem +27 -0
- data/lib/oauth/auth_sanitizer.rb +36 -0
- data/lib/oauth/client/action_controller_request.rb +22 -13
- data/lib/oauth/client/em_http.rb +106 -99
- data/lib/oauth/client/helper.rb +15 -11
- data/lib/oauth/client/net_http.rb +39 -13
- data/lib/oauth/consumer.rb +194 -75
- data/lib/oauth/errors/problem.rb +1 -1
- data/lib/oauth/helper.rb +25 -3
- data/lib/oauth/oauth.rb +28 -6
- data/lib/oauth/optional.rb +20 -0
- data/lib/oauth/request_proxy/action_controller_request.rb +11 -7
- data/lib/oauth/request_proxy/action_dispatch_request.rb +34 -0
- data/lib/oauth/request_proxy/base.rb +38 -27
- data/lib/oauth/request_proxy/em_http_request.rb +53 -52
- data/lib/oauth/request_proxy/jabber_request.rb +9 -2
- data/lib/oauth/request_proxy/net_http.rb +1 -1
- data/lib/oauth/request_proxy/rack_request.rb +0 -4
- data/lib/oauth/request_proxy/rest_client_request.rb +4 -3
- data/lib/oauth/request_proxy.rb +17 -13
- data/lib/oauth/server.rb +12 -8
- data/lib/oauth/signature/base.rb +15 -5
- data/lib/oauth/signature/rsa/sha1.rb +11 -4
- data/lib/oauth/signature.rb +43 -39
- data/lib/oauth/tokens/access_token.rb +1 -1
- data/lib/oauth/tokens/consumer_token.rb +8 -6
- data/lib/oauth/tokens/request_token.rb +9 -4
- data/lib/oauth/tokens/token.rb +13 -1
- data/lib/oauth/version.rb +2 -1
- data/lib/oauth.rb +2 -0
- data/sig/oauth/consumer.rbs +9 -0
- data/sig/oauth/signature/base.rbs +12 -0
- data/sig/oauth/tokens/token.rbs +8 -0
- data/sig/oauth/version.rbs +6 -0
- data.tar.gz.sig +0 -0
- metadata +316 -79
- metadata.gz.sig +0 -0
- data/LICENSE +0 -22
- data/TODO +0 -32
data/lib/oauth/consumer.rb
CHANGED
|
@@ -8,7 +8,22 @@ require "oauth/errors"
|
|
|
8
8
|
require "cgi"
|
|
9
9
|
|
|
10
10
|
module OAuth
|
|
11
|
+
# Consumer credentials and request configuration for OAuth 1.0 / 1.0a flows.
|
|
12
|
+
#
|
|
13
|
+
# Includes {OAuth::AUTH_SANITIZER::FilteredAttributes} so inspect output redacts the
|
|
14
|
+
# consumer secret while leaving non-sensitive configuration visible.
|
|
11
15
|
class Consumer
|
|
16
|
+
include OAuth::AUTH_SANITIZER::FilteredAttributes
|
|
17
|
+
|
|
18
|
+
# Instance attributes exposed by the consumer.
|
|
19
|
+
#
|
|
20
|
+
# @!attribute [rw] options
|
|
21
|
+
# @return [Hash] Consumer configuration options
|
|
22
|
+
# @!attribute [rw] key
|
|
23
|
+
# @return [String] OAuth consumer key
|
|
24
|
+
# @!attribute [rw] secret
|
|
25
|
+
# @return [String] OAuth consumer secret (redacted in `#inspect`)
|
|
26
|
+
|
|
12
27
|
# determine the certificate authority path to verify SSL certs
|
|
13
28
|
if ENV["SSL_CERT_FILE"]
|
|
14
29
|
if File.exist?(ENV["SSL_CERT_FILE"])
|
|
@@ -19,8 +34,11 @@ module OAuth
|
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
unless defined?(CA_FILE)
|
|
22
|
-
CA_FILES = %w[
|
|
23
|
-
|
|
37
|
+
CA_FILES = %w[
|
|
38
|
+
/etc/ssl/certs/ca-certificates.crt
|
|
39
|
+
/etc/pki/tls/certs/ca-bundle.crt
|
|
40
|
+
/usr/share/curl/curl-ca-bundle.crt
|
|
41
|
+
].freeze
|
|
24
42
|
CA_FILES.each do |ca_file|
|
|
25
43
|
if File.exist?(ca_file)
|
|
26
44
|
CA_FILE = ca_file
|
|
@@ -70,12 +88,19 @@ module OAuth
|
|
|
70
88
|
# spec. Possible values are true and false
|
|
71
89
|
body_hash_enabled: true,
|
|
72
90
|
|
|
73
|
-
oauth_version: "1.0"
|
|
74
|
-
|
|
91
|
+
oauth_version: "1.0",
|
|
92
|
+
|
|
93
|
+
# Token endpoint redirects are followed only within the same origin by
|
|
94
|
+
# default. Cross-origin redirects can re-sign token requests for an
|
|
95
|
+
# attacker-controlled endpoint, so they require explicit opt-in.
|
|
96
|
+
token_request_max_redirects: 10,
|
|
97
|
+
token_request_cross_origin_redirects: false,
|
|
98
|
+
},
|
|
75
99
|
)
|
|
76
100
|
|
|
77
101
|
attr_accessor :options, :key, :secret
|
|
78
|
-
|
|
102
|
+
filtered_attributes :secret
|
|
103
|
+
attr_writer :site, :http
|
|
79
104
|
|
|
80
105
|
# Create a new consumer instance by passing it a configuration hash:
|
|
81
106
|
#
|
|
@@ -101,7 +126,7 @@ module OAuth
|
|
|
101
126
|
# @photos=@access_token.get('/photos.xml')
|
|
102
127
|
#
|
|
103
128
|
def initialize(consumer_key, consumer_secret, options = {})
|
|
104
|
-
@key
|
|
129
|
+
@key = consumer_key
|
|
105
130
|
@secret = consumer_secret
|
|
106
131
|
|
|
107
132
|
# ensure that keys are symbols
|
|
@@ -116,12 +141,12 @@ module OAuth
|
|
|
116
141
|
|
|
117
142
|
def debug_output
|
|
118
143
|
@debug_output ||= case @options[:debug_output]
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
when nil, false
|
|
145
|
+
when true
|
|
146
|
+
$stdout
|
|
147
|
+
else
|
|
148
|
+
@options[:debug_output]
|
|
149
|
+
end
|
|
125
150
|
end
|
|
126
151
|
|
|
127
152
|
# The HTTP object for the site. The HTTP Object is what you get when you do Net::HTTP.new
|
|
@@ -132,51 +157,93 @@ module OAuth
|
|
|
132
157
|
# Contains the root URI for this site
|
|
133
158
|
def uri(custom_uri = nil)
|
|
134
159
|
if custom_uri
|
|
135
|
-
@uri
|
|
160
|
+
@uri = custom_uri
|
|
136
161
|
@http = create_http # yike, oh well. less intrusive this way
|
|
137
162
|
else # if no custom passed, we use existing, which, if unset, is set to site uri
|
|
138
163
|
@uri ||= URI.parse(site)
|
|
139
164
|
end
|
|
140
165
|
end
|
|
141
166
|
|
|
167
|
+
# Exchanges a verified Request Token for an Access Token.
|
|
168
|
+
#
|
|
169
|
+
# OAuth 1.0 vs 1.0a:
|
|
170
|
+
# - 1.0a requires including oauth_verifier (as returned by the Provider after
|
|
171
|
+
# user authorization) when performing this exchange in a 3‑legged flow.
|
|
172
|
+
# - 1.0 flows did not include oauth_verifier.
|
|
173
|
+
#
|
|
174
|
+
# Usage (3‑legged):
|
|
175
|
+
# access_token = request_token.get_access_token(oauth_verifier: params[:oauth_verifier])
|
|
176
|
+
#
|
|
177
|
+
# @param request_token [OAuth::RequestToken] The authorized request token
|
|
178
|
+
# @param request_options [Hash] OAuth or request options (include :oauth_verifier for 1.0a)
|
|
179
|
+
# @param arguments [Array] Optional POST body and headers
|
|
180
|
+
# @yield [response_body] If a block is given, yields the raw response body.
|
|
181
|
+
# @return [OAuth::AccessToken]
|
|
142
182
|
def get_access_token(request_token, request_options = {}, *arguments, &block)
|
|
143
|
-
response = token_request(
|
|
144
|
-
|
|
183
|
+
response = token_request(
|
|
184
|
+
http_method,
|
|
185
|
+
(access_token_url? ? access_token_url : access_token_path),
|
|
186
|
+
request_token,
|
|
187
|
+
request_options,
|
|
188
|
+
*arguments,
|
|
189
|
+
&block
|
|
190
|
+
)
|
|
145
191
|
OAuth::AccessToken.from_hash(self, response)
|
|
146
192
|
end
|
|
147
193
|
|
|
148
194
|
# Makes a request to the service for a new OAuth::RequestToken
|
|
149
195
|
#
|
|
150
|
-
#
|
|
196
|
+
# Example:
|
|
197
|
+
# @request_token = @consumer.get_request_token
|
|
151
198
|
#
|
|
152
199
|
# To include OAuth parameters:
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
200
|
+
# @request_token = @consumer.get_request_token(
|
|
201
|
+
# oauth_callback: "http://example.com/cb"
|
|
202
|
+
# )
|
|
156
203
|
#
|
|
157
204
|
# To include application-specific parameters:
|
|
205
|
+
# @request_token = @consumer.get_request_token({}, foo: "bar")
|
|
158
206
|
#
|
|
159
|
-
#
|
|
207
|
+
# OAuth 1.0 vs 1.0a:
|
|
208
|
+
# - In 1.0a, the Consumer SHOULD send oauth_callback when obtaining a request token
|
|
209
|
+
# (or explicitly use OUT_OF_BAND) and the Provider MUST include
|
|
210
|
+
# oauth_callback_confirmed=true in the response.
|
|
211
|
+
# - This library defaults oauth_callback to OUT_OF_BAND ("oob") when not provided,
|
|
212
|
+
# which works for both 1.0 and 1.0a, and mirrors common provider behavior.
|
|
213
|
+
# - The oauth_callback_confirmed response is parsed by the token classes; it is not
|
|
214
|
+
# part of the signature base string and thus is not signed.
|
|
160
215
|
#
|
|
161
|
-
# TODO oauth_callback
|
|
216
|
+
# TODO: In a future major release, oauth_callback may be made mandatory unless
|
|
217
|
+
# request_options[:exclude_callback] is set, to reflect 1.0a guidance.
|
|
218
|
+
#
|
|
219
|
+
# @param request_options [Hash] OAuth options for the request. Notably
|
|
220
|
+
# :oauth_callback can be set to a URL, or OAuth::OUT_OF_BAND ("oob").
|
|
221
|
+
# @param arguments [Array] Optional POST body and headers
|
|
222
|
+
# @yield [response_body] If a block is given, yields the raw response body.
|
|
223
|
+
# @return [OAuth::RequestToken]
|
|
162
224
|
def get_request_token(request_options = {}, *arguments, &block)
|
|
163
225
|
# if oauth_callback wasn't provided, it is assumed that oauth_verifiers
|
|
164
226
|
# will be exchanged out of band
|
|
165
227
|
request_options[:oauth_callback] ||= OAuth::OUT_OF_BAND unless request_options[:exclude_callback]
|
|
166
228
|
|
|
167
229
|
response = if block
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
230
|
+
token_request(
|
|
231
|
+
http_method,
|
|
232
|
+
(request_token_url? ? request_token_url : request_token_path),
|
|
233
|
+
nil,
|
|
234
|
+
request_options,
|
|
235
|
+
*arguments,
|
|
236
|
+
&block
|
|
237
|
+
)
|
|
238
|
+
else
|
|
239
|
+
token_request(
|
|
240
|
+
http_method,
|
|
241
|
+
(request_token_url? ? request_token_url : request_token_path),
|
|
242
|
+
nil,
|
|
243
|
+
request_options,
|
|
244
|
+
*arguments,
|
|
245
|
+
)
|
|
246
|
+
end
|
|
180
247
|
OAuth::RequestToken.from_hash(self, response)
|
|
181
248
|
end
|
|
182
249
|
|
|
@@ -191,23 +258,23 @@ module OAuth
|
|
|
191
258
|
# @consumer.request(:post, '/people', @token, {}, @person.to_xml, { 'Content-Type' => 'application/xml' })
|
|
192
259
|
#
|
|
193
260
|
def request(http_method, path, token = nil, request_options = {}, *arguments)
|
|
194
|
-
unless %r{^/}
|
|
261
|
+
unless %r{^/} =~ path
|
|
195
262
|
@http = create_http(path)
|
|
196
|
-
|
|
197
|
-
path = "#{
|
|
263
|
+
uri = URI.parse(path)
|
|
264
|
+
path = "#{uri.path}#{"?#{uri.query}" if uri.query}"
|
|
198
265
|
end
|
|
199
266
|
|
|
200
267
|
# override the request with your own, this is useful for file uploads which Net::HTTP does not do
|
|
201
268
|
req = create_signed_request(http_method, path, token, request_options, *arguments)
|
|
202
|
-
return
|
|
269
|
+
return if block_given? && (yield(req) == :done)
|
|
203
270
|
|
|
204
271
|
rsp = http.request(req)
|
|
205
272
|
# check for an error reported by the Problem Reporting extension
|
|
206
273
|
# (https://wiki.oauth.net/ProblemReporting)
|
|
207
274
|
# note: a 200 may actually be an error; check for an oauth_problem key to be sure
|
|
208
275
|
if !(headers = rsp.to_hash["www-authenticate"]).nil? &&
|
|
209
|
-
|
|
210
|
-
|
|
276
|
+
(h = headers.grep(/^OAuth /)).any? &&
|
|
277
|
+
h.first.include?("oauth_problem")
|
|
211
278
|
|
|
212
279
|
# puts "Header: #{h.first}"
|
|
213
280
|
|
|
@@ -233,37 +300,34 @@ module OAuth
|
|
|
233
300
|
end
|
|
234
301
|
|
|
235
302
|
# Creates a request and parses the result as url_encoded. This is used internally for the RequestToken and AccessToken requests.
|
|
236
|
-
def token_request(http_method, path, token = nil, request_options = {}, *arguments)
|
|
237
|
-
request_options
|
|
238
|
-
response = request(http_method, path, token, request_options, *arguments)
|
|
303
|
+
def token_request(http_method, path, token = nil, request_options = {}, *arguments, &block)
|
|
304
|
+
response = request(http_method, path, token, token_request_options(request_options), *arguments)
|
|
239
305
|
case response.code.to_i
|
|
240
306
|
|
|
241
307
|
when (200..299)
|
|
242
|
-
if
|
|
243
|
-
|
|
308
|
+
if block
|
|
309
|
+
block.call(response.body)
|
|
244
310
|
else
|
|
245
311
|
# symbolize keys
|
|
246
312
|
# TODO this could be considered unexpected behavior; symbols or not?
|
|
247
313
|
# TODO this also drops subsequent values from multi-valued keys
|
|
248
314
|
CGI.parse(response.body).each_with_object({}) do |(k, v), h|
|
|
249
315
|
h[k.strip.to_sym] = v.first
|
|
250
|
-
h[k.strip]
|
|
316
|
+
h[k.strip] = v.first
|
|
251
317
|
end
|
|
252
318
|
end
|
|
253
319
|
when (300..399)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
320
|
+
current_uri = token_request_uri(path)
|
|
321
|
+
redirected_uri = token_request_redirect_uri(current_uri, response)
|
|
322
|
+
response.error! unless redirected_uri
|
|
257
323
|
|
|
258
|
-
|
|
259
|
-
response.error! if
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
options[:site] = "#{uri.scheme}://#{uri.host}"
|
|
263
|
-
@http = create_http
|
|
264
|
-
end
|
|
324
|
+
redirect_count = request_options[:token_request_redirect_count].to_i + 1
|
|
325
|
+
response.error! if redirect_count > token_request_max_redirects(request_options)
|
|
326
|
+
response.error! if token_request_cross_origin?(current_uri, redirected_uri) &&
|
|
327
|
+
!token_request_cross_origin_redirects?(request_options)
|
|
265
328
|
|
|
266
|
-
|
|
329
|
+
redirect_options = request_options.merge(token_request_redirect_count: redirect_count)
|
|
330
|
+
token_request(http_method, token_request_redirect_path(current_uri, redirected_uri), token, redirect_options, *arguments, &block)
|
|
267
331
|
when (400..499)
|
|
268
332
|
raise OAuth::Unauthorized, response
|
|
269
333
|
else
|
|
@@ -271,6 +335,55 @@ module OAuth
|
|
|
271
335
|
end
|
|
272
336
|
end
|
|
273
337
|
|
|
338
|
+
def token_request_options(request_options)
|
|
339
|
+
request_options.merge(token_request: true).tap do |options|
|
|
340
|
+
options.delete(:token_request_redirect_count)
|
|
341
|
+
options.delete(:token_request_max_redirects)
|
|
342
|
+
options.delete(:token_request_cross_origin_redirects)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def token_request_uri(path)
|
|
347
|
+
uri = URI.parse(path)
|
|
348
|
+
return uri if uri.absolute?
|
|
349
|
+
|
|
350
|
+
URI.parse(site).merge(path)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def token_request_redirect_uri(current_uri, response)
|
|
354
|
+
location = response["location"]
|
|
355
|
+
return if location.nil? || location.to_s.empty?
|
|
356
|
+
|
|
357
|
+
current_uri.merge(location)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def token_request_redirect_path(current_uri, redirected_uri)
|
|
361
|
+
return redirected_uri.to_s if token_request_cross_origin?(current_uri, redirected_uri)
|
|
362
|
+
|
|
363
|
+
redirected_uri.request_uri
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def token_request_max_redirects(request_options)
|
|
367
|
+
request_options[:token_request_max_redirects] || options[:token_request_max_redirects]
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def token_request_cross_origin_redirects?(request_options)
|
|
371
|
+
request_options.fetch(:token_request_cross_origin_redirects, options[:token_request_cross_origin_redirects])
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def token_request_cross_origin?(current_uri, redirected_uri)
|
|
375
|
+
current_uri.scheme.to_s.downcase != redirected_uri.scheme.to_s.downcase ||
|
|
376
|
+
current_uri.host.to_s.downcase != redirected_uri.host.to_s.downcase ||
|
|
377
|
+
token_request_effective_port(current_uri) != token_request_effective_port(redirected_uri)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def token_request_effective_port(uri)
|
|
381
|
+
return uri.port if uri.port
|
|
382
|
+
return 443 if uri.scheme == "https"
|
|
383
|
+
|
|
384
|
+
80 if uri.scheme == "http"
|
|
385
|
+
end
|
|
386
|
+
|
|
274
387
|
# Sign the Request object. Use this if you have an externally generated http request object you want to sign.
|
|
275
388
|
def sign!(request, token = nil, request_options = {})
|
|
276
389
|
request.oauth!(http, self, token, options.merge(request_options))
|
|
@@ -286,7 +399,7 @@ module OAuth
|
|
|
286
399
|
end
|
|
287
400
|
|
|
288
401
|
def request_endpoint
|
|
289
|
-
return
|
|
402
|
+
return if @options[:request_endpoint].nil?
|
|
290
403
|
|
|
291
404
|
@options[:request_endpoint].to_s
|
|
292
405
|
end
|
|
@@ -351,28 +464,34 @@ module OAuth
|
|
|
351
464
|
protected
|
|
352
465
|
|
|
353
466
|
# Instantiates the http object
|
|
354
|
-
def create_http(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
our_uri = if
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
467
|
+
def create_http(url = nil)
|
|
468
|
+
url = request_endpoint unless request_endpoint.nil?
|
|
469
|
+
|
|
470
|
+
our_uri = if url.nil? || url[0] =~ %r{^/}
|
|
471
|
+
URI.parse(site)
|
|
472
|
+
else
|
|
473
|
+
your_uri = URI.parse(url)
|
|
474
|
+
if your_uri.host.nil?
|
|
475
|
+
# If the _url is a path, missing the leading slash, then it won't have a host,
|
|
476
|
+
# and our_uri *must* have a host, so we parse site instead.
|
|
477
|
+
URI.parse(site)
|
|
478
|
+
else
|
|
479
|
+
your_uri
|
|
480
|
+
end
|
|
481
|
+
end
|
|
369
482
|
|
|
370
483
|
if proxy.nil?
|
|
371
484
|
http_object = Net::HTTP.new(our_uri.host, our_uri.port)
|
|
372
485
|
else
|
|
373
486
|
proxy_uri = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
|
|
374
|
-
http_object = Net::HTTP.new(
|
|
375
|
-
|
|
487
|
+
http_object = Net::HTTP.new(
|
|
488
|
+
our_uri.host,
|
|
489
|
+
our_uri.port,
|
|
490
|
+
proxy_uri.host,
|
|
491
|
+
proxy_uri.port,
|
|
492
|
+
proxy_uri.user,
|
|
493
|
+
proxy_uri.password,
|
|
494
|
+
)
|
|
376
495
|
end
|
|
377
496
|
|
|
378
497
|
http_object.use_ssl = (our_uri.scheme == "https")
|
|
@@ -453,7 +572,7 @@ module OAuth
|
|
|
453
572
|
end
|
|
454
573
|
|
|
455
574
|
def marshal_dump(*_args)
|
|
456
|
-
{
|
|
575
|
+
{key: @key, secret: @secret, options: @options}
|
|
457
576
|
end
|
|
458
577
|
|
|
459
578
|
def marshal_load(data)
|
data/lib/oauth/errors/problem.rb
CHANGED
data/lib/oauth/helper.rb
CHANGED
|
@@ -18,11 +18,33 @@ module OAuth
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def _escape(string)
|
|
21
|
-
|
|
21
|
+
# Percent-encode per RFC 3986 (unreserved: A-Z a-z 0-9 - . _ ~)
|
|
22
|
+
# Encode by byte to ensure stable behavior across Ruby versions and encodings.
|
|
23
|
+
bytes = string.to_s.b.bytes
|
|
24
|
+
bytes.map do |b|
|
|
25
|
+
ch = b.chr
|
|
26
|
+
if ch =~ OAuth::RESERVED_CHARACTERS
|
|
27
|
+
"%%%02X" % b
|
|
28
|
+
else
|
|
29
|
+
ch
|
|
30
|
+
end
|
|
31
|
+
end.join
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
def unescape(value)
|
|
25
|
-
|
|
35
|
+
# Do NOT treat "+" as space; OAuth treats '+' as a literal plus unless percent-encoded.
|
|
36
|
+
str = value.to_s.gsub("+", "%2B")
|
|
37
|
+
# Decode %HH sequences; leave malformed sequences intact.
|
|
38
|
+
decoded = str.gsub(/%([0-9A-Fa-f]{2})/) { Regexp.last_match(1).to_i(16).chr }
|
|
39
|
+
# Prefer UTF-8 when the decoded bytes form valid UTF-8; otherwise, return as binary.
|
|
40
|
+
begin
|
|
41
|
+
utf8 = decoded.dup
|
|
42
|
+
utf8.force_encoding(Encoding::UTF_8)
|
|
43
|
+
decoded = utf8 if utf8.valid_encoding?
|
|
44
|
+
rescue NameError
|
|
45
|
+
# Older Rubies without Encoding constants: keep original encoding.
|
|
46
|
+
end
|
|
47
|
+
decoded
|
|
26
48
|
end
|
|
27
49
|
|
|
28
50
|
# Generate a random key of up to +size+ bytes. The value returned is Base64 encoded with non-word
|
|
@@ -31,7 +53,7 @@ module OAuth
|
|
|
31
53
|
Base64.encode64(OpenSSL::Random.random_bytes(size)).gsub(/\W/, "")
|
|
32
54
|
end
|
|
33
55
|
|
|
34
|
-
|
|
56
|
+
alias_method :generate_nonce, :generate_key
|
|
35
57
|
|
|
36
58
|
def generate_timestamp # :nodoc:
|
|
37
59
|
Time.now.to_i.to_s
|
data/lib/oauth/oauth.rb
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module OAuth
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Out-Of-Band callback token value.
|
|
5
|
+
# OAuth 1.0 and 1.0a both support out-of-band flows, where callbacks cannot be used.
|
|
6
|
+
# See RFC 5849 (OAuth 1.0), Section 6.1.1: Obtaining an Unauthorized Request Token
|
|
7
|
+
# and the 1.0a errata. Providers treating "oob" as the callback URL indicate that
|
|
8
|
+
# the verifier (for 1.0a) will be communicated out of band to the Consumer.
|
|
6
9
|
OUT_OF_BAND = "oob"
|
|
7
10
|
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
# OAuth parameter keys this library recognizes when normalizing/signing requests.
|
|
12
|
+
# Notes on 1.0 vs 1.0a:
|
|
13
|
+
# - oauth_verifier: Introduced by OAuth 1.0a. Returned to the Consumer after user
|
|
14
|
+
# authorization and required when exchanging a Request Token for an Access Token
|
|
15
|
+
# (Section 6.3.1 in RFC 5849 / 1.0a change).
|
|
16
|
+
# - oauth_callback: Present in 1.0; 1.0a clarified that the Consumer MUST send it when
|
|
17
|
+
# obtaining a Request Token (or use "oob") and that the Service Provider MUST return
|
|
18
|
+
# oauth_callback_confirmed=true with the Request Token response to prevent session
|
|
19
|
+
# fixation attacks. Note that oauth_callback_confirmed is a response parameter, not
|
|
20
|
+
# a request signing parameter, and thus is not listed here.
|
|
21
|
+
# Other keys are common to both 1.0 and 1.0a.
|
|
22
|
+
PARAMETERS = %w[
|
|
23
|
+
oauth_callback
|
|
24
|
+
oauth_consumer_key
|
|
25
|
+
oauth_token
|
|
26
|
+
oauth_signature_method
|
|
27
|
+
oauth_timestamp
|
|
28
|
+
oauth_nonce
|
|
29
|
+
oauth_verifier
|
|
30
|
+
oauth_version
|
|
31
|
+
oauth_signature
|
|
32
|
+
oauth_body_hash
|
|
33
|
+
].freeze
|
|
12
34
|
|
|
13
35
|
# reserved character regexp, per section 5.1
|
|
14
36
|
RESERVED_CHARACTERS = /[^a-zA-Z0-9\-._~]/.freeze
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OAuth
|
|
4
|
+
# Helpers for optional, lazily loaded integrations.
|
|
5
|
+
module Optional
|
|
6
|
+
class << self
|
|
7
|
+
# Try to load EventMachine HTTP client support provided by em-http-request.
|
|
8
|
+
#
|
|
9
|
+
# Returns true if available, false if the dependency is not installed.
|
|
10
|
+
# Never raises LoadError.
|
|
11
|
+
def em_http_available?
|
|
12
|
+
# em-http-request provides "em-http" entrypoint
|
|
13
|
+
require "em-http"
|
|
14
|
+
true
|
|
15
|
+
rescue LoadError
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -16,14 +16,18 @@ module OAuth
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def uri
|
|
19
|
-
request.url
|
|
19
|
+
options[:uri] || request.url
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def parameters
|
|
23
23
|
if options[:clobber_request]
|
|
24
24
|
options[:parameters] || {}
|
|
25
25
|
else
|
|
26
|
-
params
|
|
26
|
+
# Rails proxies should expose array-style values for params to align with
|
|
27
|
+
# historical oauth gem behavior / specs. Header params remain scalars.
|
|
28
|
+
rq = wrap_values(request_params)
|
|
29
|
+
qq = wrap_values(query_params)
|
|
30
|
+
params = rq.merge(qq).merge(header_params)
|
|
27
31
|
params.stringify_keys! if params.respond_to?(:stringify_keys!)
|
|
28
32
|
params.merge(options[:parameters] || {})
|
|
29
33
|
end
|
|
@@ -43,11 +47,11 @@ module OAuth
|
|
|
43
47
|
params << request.raw_post if raw_post_signature?
|
|
44
48
|
end
|
|
45
49
|
|
|
46
|
-
params
|
|
47
|
-
join("&").split("&")
|
|
48
|
-
reject { |s| s.match(/\A\s*\z/) }
|
|
49
|
-
map { |p| p.split("=").map { |esc| CGI.unescape(esc) } }
|
|
50
|
-
reject { |kv| kv[0] == "oauth_signature" }
|
|
50
|
+
params
|
|
51
|
+
.join("&").split("&")
|
|
52
|
+
.reject { |s| s.match(/\A\s*\z/) }
|
|
53
|
+
.map { |p| p.split("=").map { |esc| CGI.unescape(esc) } }
|
|
54
|
+
.reject { |kv| kv[0] == "oauth_signature" }
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
def raw_post_signature?
|
|
@@ -6,6 +6,40 @@ module OAuth
|
|
|
6
6
|
module RequestProxy
|
|
7
7
|
class ActionDispatchRequest < OAuth::RequestProxy::RackRequest
|
|
8
8
|
proxies ::ActionDispatch::Request
|
|
9
|
+
|
|
10
|
+
# Prefer the explicitly provided URI, which carries scheme/host info
|
|
11
|
+
# when ActionDispatch env may be minimal in tests.
|
|
12
|
+
def uri
|
|
13
|
+
options[:uri] || super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Rails' ActionDispatch proxy should expose array-style parameters
|
|
17
|
+
# for request/query params to align with legacy oauth gem expectations.
|
|
18
|
+
def parameters
|
|
19
|
+
if options[:clobber_request]
|
|
20
|
+
options[:parameters] || {}
|
|
21
|
+
else
|
|
22
|
+
rq = wrap_values(request_params)
|
|
23
|
+
qq = wrap_values(query_params)
|
|
24
|
+
params = rq.merge(qq).merge(header_params)
|
|
25
|
+
params.merge(options[:parameters] || {})
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
def query_params
|
|
32
|
+
# ActionDispatch::Request responds to GET
|
|
33
|
+
request.GET
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def request_params
|
|
37
|
+
if request.content_type && request.content_type.to_s.downcase.start_with?("application/x-www-form-urlencoded")
|
|
38
|
+
request.POST
|
|
39
|
+
else
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
9
43
|
end
|
|
10
44
|
end
|
|
11
45
|
end
|