oauth2 2.0.18 → 2.0.23
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 +163 -5
- data/CITATION.cff +6 -6
- data/CONTRIBUTING.md +82 -31
- data/FUNDING.md +1 -1
- data/LICENSE.md +111 -0
- data/README.md +341 -733
- data/SECURITY.md +1 -4
- data/certs/pboling.pem +27 -0
- data/lib/oauth2/access_token.rb +32 -29
- data/lib/oauth2/auth_sanitizer.rb +36 -0
- data/lib/oauth2/authenticator.rb +9 -7
- data/lib/oauth2/client.rb +74 -17
- data/lib/oauth2/error.rb +10 -6
- data/lib/oauth2/filtered_attributes.rb +7 -49
- data/lib/oauth2/response.rb +30 -24
- data/lib/oauth2/version.rb +2 -1
- data/lib/oauth2.rb +39 -17
- data/sig/oauth2/filtered_attributes.rbs +6 -1
- data/sig/oauth2/sanitized_logger.rbs +32 -0
- data/sig/oauth2/thing_filter.rbs +10 -0
- data/sig/oauth2/version.rbs +1 -0
- data.tar.gz.sig +0 -0
- metadata +125 -107
- metadata.gz.sig +0 -0
- data/IRP.md +0 -107
- data/LICENSE.txt +0 -22
- data/OIDC.md +0 -167
- data/REEK +0 -0
- data/THREAT_MODEL.md +0 -85
data/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
|----------|-----------|
|
|
7
|
-
|
|
|
7
|
+
| 2.0.latest | ✅ |
|
|
8
8
|
|
|
9
9
|
## Security contact information
|
|
10
10
|
|
|
@@ -12,8 +12,6 @@ To report a security vulnerability, please use the
|
|
|
12
12
|
[Tidelift security contact](https://tidelift.com/security).
|
|
13
13
|
Tidelift will coordinate the fix and disclosure.
|
|
14
14
|
|
|
15
|
-
More detailed explanation of the process is in [IRP.md][IRP]
|
|
16
|
-
|
|
17
15
|
## Additional Support
|
|
18
16
|
|
|
19
17
|
If you are interested in support for versions older than the latest release,
|
|
@@ -21,4 +19,3 @@ please consider sponsoring the project / maintainer @ https://liberapay.com/pbol
|
|
|
21
19
|
or find other sponsorship links in the [README].
|
|
22
20
|
|
|
23
21
|
[README]: README.md
|
|
24
|
-
[IRP]: IRP.md
|
data/certs/pboling.pem
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
|
|
3
|
+
ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
|
|
4
|
+
A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
|
|
5
|
+
DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
|
|
6
|
+
LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
|
|
7
|
+
uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
|
|
8
|
+
LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
|
|
9
|
+
mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
|
|
10
|
+
coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
|
|
11
|
+
FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
|
|
12
|
+
yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
|
|
13
|
+
to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
|
|
14
|
+
qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
|
|
15
|
+
fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
|
|
16
|
+
HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
|
|
17
|
+
A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
|
|
18
|
+
ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
|
|
19
|
+
wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
|
|
20
|
+
L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
|
|
21
|
+
GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
|
|
22
|
+
kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
|
|
23
|
+
QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
|
|
24
|
+
0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
|
|
25
|
+
DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
|
|
26
|
+
L9nRqA==
|
|
27
|
+
-----END CERTIFICATE-----
|
data/lib/oauth2/access_token.rb
CHANGED
|
@@ -57,28 +57,19 @@ module OAuth2
|
|
|
57
57
|
def from_hash(client, hash)
|
|
58
58
|
fresh = hash.dup
|
|
59
59
|
# If token_name is present, then use that key name
|
|
60
|
-
key
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
no_tokens_warning(fresh, t_key)
|
|
64
|
-
t_key
|
|
65
|
-
else
|
|
66
|
-
# Otherwise, if one of the supported default keys is present, use whichever has precedence
|
|
67
|
-
supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
|
|
68
|
-
t_key = supported_keys[0]
|
|
69
|
-
extra_tokens_warning(supported_keys, t_key)
|
|
70
|
-
t_key
|
|
71
|
-
end
|
|
72
|
-
# :nocov:
|
|
73
|
-
# TODO: Get rid of this branching logic when dropping Hashie < v3.2
|
|
74
|
-
token = if !defined?(Hashie::VERSION) # i.e. <= "1.1.0"; the first Hashie to ship with a VERSION constant
|
|
75
|
-
warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
|
|
76
|
-
# There is a bug in Hashie v0, which is accounts for.
|
|
77
|
-
fresh.delete(key) || fresh[key] || ""
|
|
60
|
+
if fresh.key?(:token_name)
|
|
61
|
+
t_key = fresh[:token_name]
|
|
62
|
+
no_tokens_warning(fresh, t_key)
|
|
78
63
|
else
|
|
79
|
-
|
|
64
|
+
# Otherwise, if one of the supported default keys is present, use whichever has precedence
|
|
65
|
+
supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
|
|
66
|
+
t_key = supported_keys[0]
|
|
67
|
+
extra_tokens_warning(supported_keys, t_key)
|
|
80
68
|
end
|
|
81
69
|
# :nocov:
|
|
70
|
+
# TODO: Get rid of this branching logic when dropping Hashie < v3.2
|
|
71
|
+
token = extract_token_value(fresh, t_key)
|
|
72
|
+
# :nocov:
|
|
82
73
|
new(client, token, fresh)
|
|
83
74
|
end
|
|
84
75
|
|
|
@@ -111,6 +102,17 @@ Custom token_name (#{key}) is not found in (#{hash.keys})
|
|
|
111
102
|
You may need to set `snaky: false`. See inline documentation for more info.
|
|
112
103
|
])
|
|
113
104
|
end
|
|
105
|
+
|
|
106
|
+
# :nocov:
|
|
107
|
+
def extract_token_value(fresh, key)
|
|
108
|
+
token_value = fresh.delete(key)
|
|
109
|
+
return token_value || "" if defined?(Hashie::VERSION)
|
|
110
|
+
|
|
111
|
+
warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
|
|
112
|
+
# There is a bug in Hashie v0, which this accounts for.
|
|
113
|
+
token_value || fresh[key] || ""
|
|
114
|
+
end
|
|
115
|
+
# :nocov:
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
# Initialize an AccessToken
|
|
@@ -308,8 +310,8 @@ You may need to set `snaky: false`. See inline documentation for more info.
|
|
|
308
310
|
# TODO: Switch when dropping Ruby < 2.5 support
|
|
309
311
|
# params.transform_keys(&:to_sym) # Ruby 2.5 only
|
|
310
312
|
# Old Ruby transform_keys alternative:
|
|
311
|
-
sheesh = @params.each_with_object({}) { |(
|
|
312
|
-
memo[
|
|
313
|
+
sheesh = @params.each_with_object({}) { |(key, value), memo|
|
|
314
|
+
memo[key.to_sym] = value
|
|
313
315
|
}
|
|
314
316
|
sheesh.merge(hsh)
|
|
315
317
|
end
|
|
@@ -378,6 +380,7 @@ You may need to set `snaky: false`. See inline documentation for more info.
|
|
|
378
380
|
|
|
379
381
|
def configure_authentication!(opts, verb)
|
|
380
382
|
mode_opt = options[:mode]
|
|
383
|
+
param_name = options[:param_name]
|
|
381
384
|
mode =
|
|
382
385
|
if mode_opt.respond_to?(:call)
|
|
383
386
|
mode_opt.call(verb)
|
|
@@ -391,19 +394,19 @@ You may need to set `snaky: false`. See inline documentation for more info.
|
|
|
391
394
|
|
|
392
395
|
case mode
|
|
393
396
|
when :header
|
|
394
|
-
opts[:headers] ||= {}
|
|
395
|
-
|
|
397
|
+
request_headers = opts[:headers] ||= {}
|
|
398
|
+
request_headers.merge!(headers)
|
|
396
399
|
when :query
|
|
397
400
|
# OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
|
|
398
401
|
# Prefer the default :header mode whenever possible.
|
|
399
|
-
opts[:params] ||= {}
|
|
400
|
-
|
|
402
|
+
request_params = opts[:params] ||= {}
|
|
403
|
+
request_params[param_name] = token
|
|
401
404
|
when :body
|
|
402
|
-
opts[:body] ||= {}
|
|
403
|
-
if
|
|
404
|
-
|
|
405
|
+
request_body = opts[:body] ||= {}
|
|
406
|
+
if request_body.is_a?(Hash)
|
|
407
|
+
request_body[param_name] = token
|
|
405
408
|
else
|
|
406
|
-
opts[:body]
|
|
409
|
+
opts[:body] = "#{request_body}&#{param_name}=#{token}"
|
|
407
410
|
end
|
|
408
411
|
# @todo support for multi-part (file uploads)
|
|
409
412
|
else
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OAuth2
|
|
4
|
+
AUTH_SANITIZER = begin
|
|
5
|
+
auth_sanitizer_requirement = Gem::Requirement.new("~> 0.2", ">= 0.2.1")
|
|
6
|
+
auth_sanitizer_spec = Gem.loaded_specs["auth-sanitizer"]
|
|
7
|
+
unless auth_sanitizer_spec && auth_sanitizer_requirement.satisfied_by?(auth_sanitizer_spec.version)
|
|
8
|
+
# :nocov:
|
|
9
|
+
auth_sanitizer_spec = Gem::Specification.find_by_name("auth-sanitizer", auth_sanitizer_requirement)
|
|
10
|
+
# :nocov:
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
auth_sanitizer_loader_path = File.join(
|
|
14
|
+
auth_sanitizer_spec.full_gem_path,
|
|
15
|
+
"lib/auth_sanitizer/loader.rb"
|
|
16
|
+
)
|
|
17
|
+
unless File.file?(auth_sanitizer_loader_path)
|
|
18
|
+
# :nocov:
|
|
19
|
+
raise LoadError, "oauth2 requires auth-sanitizer #{auth_sanitizer_requirement}; " \
|
|
20
|
+
"loader not found at #{auth_sanitizer_loader_path}"
|
|
21
|
+
# :nocov:
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
auth_sanitizer_loader_namespace = Module.new
|
|
25
|
+
auth_sanitizer_loader_namespace.module_eval(
|
|
26
|
+
File.read(auth_sanitizer_loader_path),
|
|
27
|
+
auth_sanitizer_loader_path,
|
|
28
|
+
1
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
auth_sanitizer_loader_namespace.
|
|
32
|
+
const_get(:AuthSanitizer).
|
|
33
|
+
const_get(:Loader).
|
|
34
|
+
load_isolated
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/oauth2/authenticator.rb
CHANGED
|
@@ -51,13 +51,15 @@ module OAuth2
|
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
class << self
|
|
55
|
+
# Encodes a Basic Authorization header value for the provided credentials.
|
|
56
|
+
#
|
|
57
|
+
# @param [String] user The client identifier
|
|
58
|
+
# @param [String] password The client secret
|
|
59
|
+
# @return [String] The value to use for the Authorization header
|
|
60
|
+
def encode_basic_auth(user, password)
|
|
61
|
+
"Basic #{Base64.strict_encode64("#{user}:#{password}")}"
|
|
62
|
+
end
|
|
61
63
|
end
|
|
62
64
|
|
|
63
65
|
private
|
data/lib/oauth2/client.rb
CHANGED
|
@@ -42,7 +42,7 @@ module OAuth2
|
|
|
42
42
|
# @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday
|
|
43
43
|
# @option options [Boolean] :raise_errors (true) whether to raise an OAuth2::Error on responses with 400+ status codes
|
|
44
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
|
|
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
46
|
# @option options [Class] :access_token_class (AccessToken) class to use for access tokens; you can subclass OAuth2::AccessToken, @version 2.0+
|
|
47
47
|
# @option options [Hash] :ssl SSL options for Faraday
|
|
48
48
|
#
|
|
@@ -86,8 +86,9 @@ module OAuth2
|
|
|
86
86
|
@connection ||=
|
|
87
87
|
Faraday.new(site, options[:connection_opts]) do |builder|
|
|
88
88
|
oauth_debug_logging(builder)
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
connection_build = options[:connection_build]
|
|
90
|
+
if connection_build
|
|
91
|
+
connection_build.call(builder)
|
|
91
92
|
else
|
|
92
93
|
builder.request(:url_encoded) # form-encode POST params
|
|
93
94
|
builder.adapter(Faraday.default_adapter) # make requests with Net::HTTP
|
|
@@ -149,9 +150,9 @@ module OAuth2
|
|
|
149
150
|
|
|
150
151
|
case status
|
|
151
152
|
when 301, 302, 303, 307
|
|
152
|
-
req_opts[:redirect_count]
|
|
153
|
-
req_opts[:redirect_count]
|
|
154
|
-
return response if
|
|
153
|
+
redirect_count = (req_opts[:redirect_count] || 0).to_i + 1
|
|
154
|
+
req_opts[:redirect_count] = redirect_count
|
|
155
|
+
return response if redirect_count > options[:max_redirects]
|
|
155
156
|
|
|
156
157
|
if status == 303
|
|
157
158
|
verb = :get
|
|
@@ -159,8 +160,9 @@ module OAuth2
|
|
|
159
160
|
end
|
|
160
161
|
location = response.headers["location"]
|
|
161
162
|
if location
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
current_location = response.response.env.url
|
|
164
|
+
full_location = resolve_redirect_location(current_location, location)
|
|
165
|
+
request(verb, full_location, sanitize_redirect_options(req_opts, current_location, full_location))
|
|
164
166
|
else
|
|
165
167
|
error = Error.new(response)
|
|
166
168
|
raise(error, "Got #{status} status code, but no Location header was present")
|
|
@@ -337,8 +339,9 @@ module OAuth2
|
|
|
337
339
|
#
|
|
338
340
|
# @return [Hash] the params to add to a request or URL
|
|
339
341
|
def redirection_params
|
|
340
|
-
|
|
341
|
-
|
|
342
|
+
redirect_uri = options[:redirect_uri]
|
|
343
|
+
if redirect_uri
|
|
344
|
+
{"redirect_uri" => redirect_uri}
|
|
342
345
|
else
|
|
343
346
|
{}
|
|
344
347
|
end
|
|
@@ -445,18 +448,18 @@ module OAuth2
|
|
|
445
448
|
url = connection.build_url(url).to_s
|
|
446
449
|
# See: Hash#partition https://bugs.ruby-lang.org/issues/16252
|
|
447
450
|
req_opts, oauth_opts = opts.
|
|
448
|
-
partition { |
|
|
449
|
-
map
|
|
451
|
+
partition { |key, _value| RESERVED_REQ_KEYS.include?(key.to_s) }.
|
|
452
|
+
map(&:to_h)
|
|
450
453
|
|
|
451
454
|
begin
|
|
452
455
|
response = connection.run_request(verb, url, req_opts[:body], req_opts[:headers]) do |req|
|
|
453
456
|
req.params.update(req_opts[:params]) if req_opts[:params]
|
|
454
457
|
yield(req) if block_given?
|
|
455
458
|
end
|
|
456
|
-
rescue Faraday::ConnectionFailed =>
|
|
457
|
-
raise ConnectionError,
|
|
458
|
-
rescue Faraday::TimeoutError =>
|
|
459
|
-
raise TimeoutError,
|
|
459
|
+
rescue Faraday::ConnectionFailed => exception
|
|
460
|
+
raise ConnectionError, exception
|
|
461
|
+
rescue Faraday::TimeoutError => exception
|
|
462
|
+
raise TimeoutError, exception
|
|
460
463
|
end
|
|
461
464
|
|
|
462
465
|
parse = oauth_opts.key?(:parse) ? oauth_opts.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
|
|
@@ -465,6 +468,49 @@ module OAuth2
|
|
|
465
468
|
Response.new(response, parse: parse, snaky: snaky)
|
|
466
469
|
end
|
|
467
470
|
|
|
471
|
+
def resolve_redirect_location(current_location, location)
|
|
472
|
+
return protocol_relative_redirect_location(current_location, location) if location.respond_to?(:start_with?) && location.start_with?("//")
|
|
473
|
+
|
|
474
|
+
current_location.merge(location)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def protocol_relative_redirect_location(current_location, location)
|
|
478
|
+
protocol_relative_location = URI.parse(location)
|
|
479
|
+
authority = +""
|
|
480
|
+
authority << "#{protocol_relative_location.userinfo}@" if protocol_relative_location.userinfo
|
|
481
|
+
authority << protocol_relative_location.host.to_s
|
|
482
|
+
authority << ":#{protocol_relative_location.port}" if protocol_relative_location.port
|
|
483
|
+
|
|
484
|
+
current_location.dup.tap do |safe_location|
|
|
485
|
+
safe_location.path = "///#{authority}#{protocol_relative_location.path}"
|
|
486
|
+
safe_location.query = protocol_relative_location.query if safe_location.respond_to?(:query=)
|
|
487
|
+
safe_location.fragment = protocol_relative_location.fragment if safe_location.respond_to?(:fragment=)
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def sanitize_redirect_options(req_opts, current_location, next_location)
|
|
492
|
+
return req_opts unless cross_origin_redirect?(current_location, next_location)
|
|
493
|
+
|
|
494
|
+
headers = req_opts[:headers]
|
|
495
|
+
return req_opts unless headers && headers.any? { |key, _value| authorization_header?(key) }
|
|
496
|
+
|
|
497
|
+
safe_opts = req_opts.dup
|
|
498
|
+
safe_headers = headers.dup
|
|
499
|
+
safe_headers.delete_if { |key, _value| authorization_header?(key) }
|
|
500
|
+
safe_opts[:headers] = safe_headers
|
|
501
|
+
safe_opts
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def authorization_header?(key)
|
|
505
|
+
key.to_s.casecmp("Authorization").zero?
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def cross_origin_redirect?(current_location, next_location)
|
|
509
|
+
current_location.scheme != next_location.scheme ||
|
|
510
|
+
current_location.host != next_location.host ||
|
|
511
|
+
current_location.port != next_location.port
|
|
512
|
+
end
|
|
513
|
+
|
|
468
514
|
# Returns the authenticator object
|
|
469
515
|
#
|
|
470
516
|
# @return [Authenticator] the initialized Authenticator
|
|
@@ -563,7 +609,18 @@ module OAuth2
|
|
|
563
609
|
end
|
|
564
610
|
|
|
565
611
|
def oauth_debug_logging(builder)
|
|
566
|
-
|
|
612
|
+
if OAuth2::OAUTH_DEBUG
|
|
613
|
+
config = OAuth2.config
|
|
614
|
+
builder.response(
|
|
615
|
+
:logger,
|
|
616
|
+
OAuth2::AUTH_SANITIZER::SanitizedLogger.new(
|
|
617
|
+
options[:logger],
|
|
618
|
+
filtered_keys: config[:filtered_debug_keys],
|
|
619
|
+
label: config[:filtered_label]
|
|
620
|
+
),
|
|
621
|
+
bodies: true
|
|
622
|
+
)
|
|
623
|
+
end
|
|
567
624
|
end
|
|
568
625
|
end
|
|
569
626
|
end
|
data/lib/oauth2/error.rb
CHANGED
|
@@ -17,10 +17,13 @@ module OAuth2
|
|
|
17
17
|
# @param [OAuth2::Response, Hash, Object] response A Response or error payload
|
|
18
18
|
def initialize(response)
|
|
19
19
|
@response = response
|
|
20
|
+
@code = nil
|
|
21
|
+
@description = nil
|
|
20
22
|
if response.respond_to?(:parsed)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@
|
|
23
|
+
parsed_response = response.parsed
|
|
24
|
+
if parsed_response.is_a?(Hash)
|
|
25
|
+
@code = parsed_response["error"]
|
|
26
|
+
@description = parsed_response["error_description"]
|
|
24
27
|
end
|
|
25
28
|
elsif response.is_a?(Hash)
|
|
26
29
|
@code = response["error"]
|
|
@@ -44,11 +47,12 @@ module OAuth2
|
|
|
44
47
|
# @return [String] Message suitable for StandardError
|
|
45
48
|
def error_message(response_body, opts = {})
|
|
46
49
|
lines = []
|
|
50
|
+
error_description = opts[:error_description]
|
|
47
51
|
|
|
48
|
-
lines <<
|
|
52
|
+
lines << error_description if error_description
|
|
49
53
|
|
|
50
|
-
error_string = if response_body.respond_to?(:encode) &&
|
|
51
|
-
script_encoding =
|
|
54
|
+
error_string = if response_body.respond_to?(:encode) && error_description.respond_to?(:encoding)
|
|
55
|
+
script_encoding = error_description.encoding
|
|
52
56
|
response_body.encode(script_encoding, invalid: :replace, undef: :replace)
|
|
53
57
|
else
|
|
54
58
|
response_body
|
|
@@ -1,52 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module OAuth2
|
|
2
|
-
#
|
|
4
|
+
# Permanent alias for {OAuth2::AUTH_SANITIZER::FilteredAttributes}.
|
|
3
5
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
# Hook invoked when the module is included. Extends the including class with
|
|
9
|
-
# class-level helpers.
|
|
10
|
-
#
|
|
11
|
-
# @param [Class] base The including class
|
|
12
|
-
# @return [void]
|
|
13
|
-
def self.included(base)
|
|
14
|
-
base.extend(ClassMethods)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# Class-level helpers for configuring filtered attributes.
|
|
18
|
-
module ClassMethods
|
|
19
|
-
# Declare attributes that should be redacted in inspect output.
|
|
20
|
-
#
|
|
21
|
-
# @param [Array<Symbol, String>] attributes One or more attribute names
|
|
22
|
-
# @return [void]
|
|
23
|
-
def filtered_attributes(*attributes)
|
|
24
|
-
@filtered_attribute_names = attributes.map(&:to_sym)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# The configured attribute names to filter.
|
|
28
|
-
#
|
|
29
|
-
# @return [Array<Symbol>]
|
|
30
|
-
def filtered_attribute_names
|
|
31
|
-
@filtered_attribute_names || []
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Custom inspect that redacts configured attributes.
|
|
36
|
-
#
|
|
37
|
-
# @return [String]
|
|
38
|
-
def inspect
|
|
39
|
-
filtered_attribute_names = self.class.filtered_attribute_names
|
|
40
|
-
return super if filtered_attribute_names.empty?
|
|
41
|
-
|
|
42
|
-
inspected_vars = instance_variables.map do |var|
|
|
43
|
-
if filtered_attribute_names.any? { |filtered_var| var.to_s.include?(filtered_var.to_s) }
|
|
44
|
-
"#{var}=[FILTERED]"
|
|
45
|
-
else
|
|
46
|
-
"#{var}=#{instance_variable_get(var).inspect}"
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
"#<#{self.class}:#{object_id} #{inspected_vars.join(", ")}>"
|
|
50
|
-
end
|
|
51
|
-
end
|
|
6
|
+
# This constant is intentionally kept in the `OAuth2` namespace because it
|
|
7
|
+
# was part of the public API before the implementation was extracted into the
|
|
8
|
+
# `auth-sanitizer` gem. It will **not** be deprecated or removed.
|
|
9
|
+
FilteredAttributes = OAuth2::AUTH_SANITIZER::FilteredAttributes
|
|
52
10
|
end
|
data/lib/oauth2/response.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "set"
|
|
4
5
|
require "multi_xml"
|
|
5
6
|
require "rack"
|
|
6
7
|
|
|
@@ -43,18 +44,20 @@ module OAuth2
|
|
|
43
44
|
"text/plain" => :text,
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
key
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
class << self
|
|
48
|
+
# Adds a new content type parser.
|
|
49
|
+
#
|
|
50
|
+
# @param [Symbol] key A descriptive symbol key such as :json or :query
|
|
51
|
+
# @param [Array<String>, String] mime_types One or more mime types to which this parser applies
|
|
52
|
+
# @yield [String] Block that will be called to parse the response body
|
|
53
|
+
# @yieldparam [String] body The response body to parse
|
|
54
|
+
# @return [void]
|
|
55
|
+
def register_parser(key, mime_types, &block)
|
|
56
|
+
key = key.to_sym
|
|
57
|
+
@@parsers[key] = block
|
|
58
|
+
Array(mime_types).each do |mime_type|
|
|
59
|
+
@@content_types[mime_type] = key
|
|
60
|
+
end
|
|
58
61
|
end
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -106,15 +109,16 @@ module OAuth2
|
|
|
106
109
|
def parsed
|
|
107
110
|
return @parsed if defined?(@parsed)
|
|
108
111
|
|
|
112
|
+
response_parser = parser
|
|
109
113
|
@parsed =
|
|
110
|
-
if
|
|
111
|
-
case
|
|
114
|
+
if response_parser.respond_to?(:call)
|
|
115
|
+
case response_parser.arity
|
|
112
116
|
when 0
|
|
113
|
-
|
|
117
|
+
response_parser.call
|
|
114
118
|
when 1
|
|
115
|
-
|
|
119
|
+
response_parser.call(body)
|
|
116
120
|
else
|
|
117
|
-
|
|
121
|
+
response_parser.call(body, response)
|
|
118
122
|
end
|
|
119
123
|
end
|
|
120
124
|
|
|
@@ -130,9 +134,10 @@ module OAuth2
|
|
|
130
134
|
#
|
|
131
135
|
# @return [String, nil] The content type or nil if headers are not present
|
|
132
136
|
def content_type
|
|
133
|
-
|
|
137
|
+
response_headers = response.headers
|
|
138
|
+
return unless response_headers
|
|
134
139
|
|
|
135
|
-
((
|
|
140
|
+
((response_headers.values_at("content-type", "Content-Type").compact.first || "").split(";").first || "").strip.downcase
|
|
136
141
|
end
|
|
137
142
|
|
|
138
143
|
# Determines the parser to be used for the response body
|
|
@@ -152,11 +157,12 @@ module OAuth2
|
|
|
152
157
|
def parser
|
|
153
158
|
return @parser if defined?(@parser)
|
|
154
159
|
|
|
160
|
+
parse_option = options[:parse]
|
|
155
161
|
@parser =
|
|
156
|
-
if
|
|
157
|
-
|
|
158
|
-
elsif
|
|
159
|
-
@@parsers[
|
|
162
|
+
if parse_option.respond_to?(:call)
|
|
163
|
+
parse_option
|
|
164
|
+
elsif parse_option
|
|
165
|
+
@@parsers[parse_option.to_sym]
|
|
160
166
|
end
|
|
161
167
|
|
|
162
168
|
@parser ||= @@parsers[@@content_types[content_type]]
|
|
@@ -169,7 +175,7 @@ end
|
|
|
169
175
|
OAuth2::Response.register_parser(:xml, ["text/xml", "application/rss+xml", "application/rdf+xml", "application/atom+xml", "application/xml"]) do |body|
|
|
170
176
|
next body unless body.respond_to?(:to_str)
|
|
171
177
|
|
|
172
|
-
MultiXml.parse(body)
|
|
178
|
+
(defined?(MultiXML) ? MultiXML : MultiXml).parse(body)
|
|
173
179
|
end
|
|
174
180
|
|
|
175
181
|
# Register JSON parser
|
data/lib/oauth2/version.rb
CHANGED
data/lib/oauth2.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# includes modules from stdlib
|
|
4
|
-
require "cgi"
|
|
4
|
+
require "cgi/escape"
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
7
|
# third party gems
|
|
@@ -10,6 +10,7 @@ require "version_gem"
|
|
|
10
10
|
|
|
11
11
|
# includes gem files
|
|
12
12
|
require_relative "oauth2/version"
|
|
13
|
+
require_relative "oauth2/auth_sanitizer"
|
|
13
14
|
require_relative "oauth2/filtered_attributes"
|
|
14
15
|
require_relative "oauth2/error"
|
|
15
16
|
require_relative "oauth2/authenticator"
|
|
@@ -43,38 +44,59 @@ module OAuth2
|
|
|
43
44
|
# config[:silence_no_tokens_warning] = false
|
|
44
45
|
# end
|
|
45
46
|
#
|
|
47
|
+
# @example Customize filtered output markers and debug-log value filtering by key name
|
|
48
|
+
# OAuth2.configure do |config|
|
|
49
|
+
# config[:filtered_label] = "[REDACTED]"
|
|
50
|
+
# config[:filtered_debug_keys] += ["client_assertion"]
|
|
51
|
+
# end
|
|
52
|
+
#
|
|
53
|
+
# Existing objects and logger wrappers snapshot filtering configuration during
|
|
54
|
+
# initialization. Changing these config values later affects only newly
|
|
55
|
+
# initialized objects and debug loggers.
|
|
56
|
+
#
|
|
46
57
|
# @return [SnakyHash::SymbolKeyed] A mutable Hash-like config with symbol keys
|
|
47
58
|
DEFAULT_CONFIG = SnakyHash::SymbolKeyed.new(
|
|
48
59
|
silence_extra_tokens_warning: true,
|
|
49
60
|
silence_no_tokens_warning: true,
|
|
61
|
+
filtered_label: "[FILTERED]",
|
|
62
|
+
filtered_debug_keys: %w[
|
|
63
|
+
access_token
|
|
64
|
+
refresh_token
|
|
65
|
+
id_token
|
|
66
|
+
client_secret
|
|
67
|
+
assertion
|
|
68
|
+
code_verifier
|
|
69
|
+
token
|
|
70
|
+
]
|
|
50
71
|
)
|
|
51
72
|
|
|
52
73
|
# The current runtime configuration for the library.
|
|
53
74
|
#
|
|
54
75
|
# @return [SnakyHash::SymbolKeyed]
|
|
55
|
-
|
|
76
|
+
CONFIG = DEFAULT_CONFIG.dup
|
|
56
77
|
|
|
57
78
|
class << self
|
|
58
|
-
|
|
79
|
+
def config
|
|
80
|
+
CONFIG
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Configure global library behavior.
|
|
59
84
|
#
|
|
60
|
-
#
|
|
85
|
+
# Yields the mutable configuration object so callers can update settings.
|
|
61
86
|
#
|
|
62
|
-
# @
|
|
63
|
-
|
|
87
|
+
# @yieldparam [SnakyHash::SymbolKeyed] config the configuration object
|
|
88
|
+
# @return [void]
|
|
89
|
+
def configure
|
|
90
|
+
yield config
|
|
91
|
+
end
|
|
64
92
|
end
|
|
65
|
-
|
|
66
|
-
# Configure global library behavior.
|
|
67
|
-
#
|
|
68
|
-
# Yields the mutable configuration object so callers can update settings.
|
|
69
|
-
#
|
|
70
|
-
# @yieldparam [SnakyHash::SymbolKeyed] config the configuration object
|
|
71
|
-
# @return [void]
|
|
72
|
-
def configure
|
|
73
|
-
yield @config
|
|
74
|
-
end
|
|
75
|
-
module_function :configure
|
|
76
93
|
end
|
|
77
94
|
|
|
95
|
+
# Wire OAuth2::AUTH_SANITIZER's label provider to read from OAuth2.config so that
|
|
96
|
+
# FilteredAttributes-bearing objects and OAuth2::AUTH_SANITIZER::SanitizedLogger instances
|
|
97
|
+
# pick up OAuth2.config[:filtered_label] at their initialization time.
|
|
98
|
+
OAuth2::AUTH_SANITIZER.filtered_label_provider = -> { OAuth2.config[:filtered_label] }
|
|
99
|
+
|
|
78
100
|
# Extend OAuth2::Version with VersionGem helpers to provide semantic version helpers.
|
|
79
101
|
OAuth2::Version.class_eval do
|
|
80
102
|
extend VersionGem::Basic
|