oauth2 2.0.18 → 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.
data/SECURITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  | Version | Supported |
6
6
  |----------|-----------|
7
- | 1.latest | ✅ |
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-----
@@ -57,26 +57,23 @@ 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
- if fresh.key?(:token_name)
62
- t_key = fresh[:token_name]
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
60
+ if fresh.key?(:token_name)
61
+ t_key = fresh[:token_name]
62
+ no_tokens_warning(fresh, t_key)
63
+ else
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)
68
+ end
72
69
  # :nocov:
73
70
  # TODO: Get rid of this branching logic when dropping Hashie < v3.2
74
71
  token = if !defined?(Hashie::VERSION) # i.e. <= "1.1.0"; the first Hashie to ship with a VERSION constant
75
72
  warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
76
73
  # There is a bug in Hashie v0, which is accounts for.
77
- fresh.delete(key) || fresh[key] || ""
74
+ fresh.delete(t_key) || fresh[t_key] || ""
78
75
  else
79
- fresh.delete(key) || ""
76
+ fresh.delete(t_key) || ""
80
77
  end
81
78
  # :nocov:
82
79
  new(client, token, fresh)
@@ -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
@@ -51,13 +51,15 @@ module OAuth2
51
51
  end
52
52
  end
53
53
 
54
- # Encodes a Basic Authorization header value for the provided credentials.
55
- #
56
- # @param [String] user The client identifier
57
- # @param [String] password The client secret
58
- # @return [String] The value to use for the Authorization header
59
- def self.encode_basic_auth(user, password)
60
- "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
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
  #
@@ -159,8 +159,9 @@ module OAuth2
159
159
  end
160
160
  location = response.headers["location"]
161
161
  if location
162
- full_location = response.response.env.url.merge(location)
163
- request(verb, full_location, req_opts)
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))
164
165
  else
165
166
  error = Error.new(response)
166
167
  raise(error, "Got #{status} status code, but no Location header was present")
@@ -446,7 +447,7 @@ module OAuth2
446
447
  # See: Hash#partition https://bugs.ruby-lang.org/issues/16252
447
448
  req_opts, oauth_opts = opts.
448
449
  partition { |k, _v| RESERVED_REQ_KEYS.include?(k.to_s) }.
449
- map { |p| Hash[p] }
450
+ map(&:to_h)
450
451
 
451
452
  begin
452
453
  response = connection.run_request(verb, url, req_opts[:body], req_opts[:headers]) do |req|
@@ -465,6 +466,36 @@ module OAuth2
465
466
  Response.new(response, parse: parse, snaky: snaky)
466
467
  end
467
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
+
468
499
  # Returns the authenticator object
469
500
  #
470
501
  # @return [Authenticator] the initialized Authenticator
@@ -563,7 +594,17 @@ module OAuth2
563
594
  end
564
595
 
565
596
  def oauth_debug_logging(builder)
566
- builder.response(:logger, options[:logger], bodies: true) if OAuth2::OAUTH_DEBUG
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
567
608
  end
568
609
  end
569
610
  end
data/lib/oauth2/error.rb CHANGED
@@ -17,6 +17,8 @@ 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
23
  if response.parsed.is_a?(Hash)
22
24
  @code = response.parsed["error"]
@@ -1,52 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
- # Mixin that redacts sensitive instance variables in #inspect output.
4
+ # Permanent alias for {OAuth2::AUTH_SANITIZER::FilteredAttributes}.
3
5
  #
4
- # Classes include this module and declare which attributes should be filtered
5
- # using {.filtered_attributes}. Any instance variable name that includes one of
6
- # those attribute names will be shown as [FILTERED] in the object's inspect.
7
- module FilteredAttributes
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
@@ -43,18 +43,20 @@ module OAuth2
43
43
  "text/plain" => :text,
44
44
  }
45
45
 
46
- # Adds a new content type parser.
47
- #
48
- # @param [Symbol] key A descriptive symbol key such as :json or :query
49
- # @param [Array<String>, String] mime_types One or more mime types to which this parser applies
50
- # @yield [String] Block that will be called to parse the response body
51
- # @yieldparam [String] body The response body to parse
52
- # @return [void]
53
- def self.register_parser(key, mime_types, &block)
54
- key = key.to_sym
55
- @@parsers[key] = block
56
- Array(mime_types).each do |mime_type|
57
- @@content_types[mime_type] = key
46
+ class << self
47
+ # Adds a new content type parser.
48
+ #
49
+ # @param [Symbol] key A descriptive symbol key such as :json or :query
50
+ # @param [Array<String>, String] mime_types One or more mime types to which this parser applies
51
+ # @yield [String] Block that will be called to parse the response body
52
+ # @yieldparam [String] body The response body to parse
53
+ # @return [void]
54
+ def register_parser(key, mime_types, &block)
55
+ key = key.to_sym
56
+ @@parsers[key] = block
57
+ Array(mime_types).each do |mime_type|
58
+ @@content_types[mime_type] = key
59
+ end
58
60
  end
59
61
  end
60
62
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module OAuth2
4
4
  module Version
5
- VERSION = "2.0.18"
5
+ VERSION = "2.0.22"
6
6
  end
7
+ VERSION = Version::VERSION # Traditional Constant Location
7
8
  end
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
- @config = DEFAULT_CONFIG.dup
76
+ CONFIG = DEFAULT_CONFIG.dup
56
77
 
57
78
  class << self
58
- # Access the current configuration.
79
+ def config
80
+ CONFIG
81
+ end
82
+
83
+ # Configure global library behavior.
59
84
  #
60
- # Prefer using {OAuth2.configure} to mutate configuration.
85
+ # Yields the mutable configuration object so callers can update settings.
61
86
  #
62
- # @return [SnakyHash::SymbolKeyed]
63
- attr_reader :config
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
@@ -1,6 +1,11 @@
1
1
  module OAuth2
2
2
  module FilteredAttributes
3
+ module InitializerMethods
4
+ def initialize: (*untyped args) { () -> untyped } -> untyped
5
+ end
6
+
3
7
  def self.included: (untyped) -> untyped
4
- def filtered_attributes: (*String) -> void
8
+ def filtered_attributes: (*(String | Symbol)) -> void
9
+ def thing_filter: () -> OAuth2::ThingFilter
5
10
  end
6
11
  end
@@ -0,0 +1,32 @@
1
+ module OAuth2
2
+ class SanitizedLogger
3
+ def initialize: (untyped logger) -> void
4
+
5
+ def add: (untyped severity, ?untyped message, ?untyped progname) { () -> untyped } -> untyped
6
+ def <<: (String message) -> untyped
7
+ def debug: (?untyped progname) { () -> untyped } -> untyped
8
+ def info: (?untyped progname) { () -> untyped } -> untyped
9
+ def warn: (?untyped progname) { () -> untyped } -> untyped
10
+ def error: (?untyped progname) { () -> untyped } -> untyped
11
+ def fatal: (?untyped progname) { () -> untyped } -> untyped
12
+ def unknown: (?untyped progname) { () -> untyped } -> untyped
13
+ def close: () -> void
14
+ def formatter: () -> untyped
15
+ def formatter=: (untyped formatter) -> void
16
+ def level: () -> untyped
17
+ def level=: (untyped level) -> void
18
+ def progname: () -> untyped
19
+ def progname=: (untyped progname) -> void
20
+ def respond_to_missing?: (Symbol method_name, ?bool include_private) -> bool
21
+ def method_missing: (Symbol method_name, *untyped args) { () -> untyped } -> untyped
22
+
23
+ private
24
+
25
+ def log: (Symbol level, ?untyped progname) { () -> untyped } -> untyped
26
+ def sanitize: (untyped message) -> untyped
27
+ def thing_filter: () -> OAuth2::ThingFilter
28
+ def sanitize_authorization_header: (String message) -> String
29
+ def sanitize_json_pairs: (String message) -> String
30
+ def sanitize_form_and_query_pairs: (String message) -> String
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ module OAuth2
2
+ class ThingFilter
3
+ attr_reader things: Array[String]
4
+ attr_reader label: String
5
+
6
+ def initialize: (Enumerable[untyped] things, label: String) -> void
7
+ def filtered?: (untyped thing_name) -> bool
8
+ def pattern_source: () -> String
9
+ end
10
+ end
@@ -2,4 +2,5 @@ module OAuth2
2
2
  module Version
3
3
  VERSION: String
4
4
  end
5
+ VERSION: String
5
6
  end
data.tar.gz.sig CHANGED
Binary file