oauth2 2.0.17 → 2.0.19

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/REEK CHANGED
@@ -0,0 +1,2 @@
1
+ ./reek: 1: Error:: not found
2
+ ./reek: 2: Error:: not found
data/RUBOCOP.md CHANGED
File without changes
data/SECURITY.md CHANGED
@@ -12,6 +12,8 @@ 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
+
15
17
  ## Additional Support
16
18
 
17
19
  If you are interested in support for versions older than the latest release,
@@ -19,3 +21,4 @@ please consider sponsoring the project / maintainer @ https://liberapay.com/pbol
19
21
  or find other sponsorship links in the [README].
20
22
 
21
23
  [README]: README.md
24
+ [IRP]: IRP.md
data/THREAT_MODEL.md ADDED
@@ -0,0 +1,94 @@
1
+ # Threat Model Outline for oauth2 Ruby Gem
2
+
3
+ ## 1. Overview
4
+ This document outlines the threat model for the `oauth2` Ruby gem, which implements OAuth 2.0, 2.1, and OIDC Core protocols. The gem is used to facilitate secure authorization and authentication in Ruby applications.
5
+
6
+ ## 2. Assets to Protect
7
+ - OAuth access tokens, refresh tokens, and ID tokens
8
+ - User credentials (if handled)
9
+ - Client secrets and application credentials
10
+ - Sensitive user data accessed via OAuth
11
+ - Private keys and certificates (for signing/verifying tokens)
12
+
13
+ ## 3. Potential Threat Actors
14
+ - External attackers (internet-based)
15
+ - Malicious OAuth clients or resource servers
16
+ - Insiders (developers, maintainers)
17
+ - Compromised dependencies
18
+
19
+ ## 4. Attack Surfaces
20
+ - OAuth endpoints (authorization, token, revocation, introspection)
21
+ - HTTP request/response handling
22
+ - Token storage and management
23
+ - Configuration files and environment variables
24
+ - Dependency supply chain
25
+
26
+ ## 5. Threats and Mitigations
27
+
28
+ ### 5.1 Token Leakage
29
+ - **Threat:** Tokens exposed via logs, URLs, or insecure storage
30
+ - **Mitigations:**
31
+ - Avoid logging sensitive tokens
32
+ - Use secure storage mechanisms
33
+ - Never expose tokens in URLs
34
+
35
+ ### 5.2 Token Replay and Forgery
36
+ - **Threat:** Attackers reuse or forge tokens
37
+ - **Mitigations:**
38
+ - Validate token signatures and claims
39
+ - Use short-lived tokens and refresh tokens
40
+ - Implement token revocation
41
+
42
+ ### 5.3 Insecure Communication
43
+ - **Threat:** Data intercepted via MITM attacks
44
+ - **Mitigations:**
45
+ - Enforce HTTPS for all communications
46
+ - Validate SSL/TLS certificates
47
+
48
+ ### 5.4 Client Secret Exposure
49
+ - **Threat:** Client secrets leaked in code or version control
50
+ - **Mitigations:**
51
+ - Store secrets in environment variables or secure vaults
52
+ - Never commit secrets to source control
53
+
54
+ ### 5.5 Dependency Vulnerabilities
55
+ - **Threat:** Vulnerabilities in third-party libraries
56
+ - **Mitigations:**
57
+ - Regularly update dependencies
58
+ - Use tools like `bundler-audit` for vulnerability scanning
59
+
60
+ ### 5.6 Improper Input Validation
61
+ - **Threat:** Injection attacks via untrusted input
62
+ - **Mitigations:**
63
+ - Validate and sanitize all inputs
64
+ - Use parameterized queries and safe APIs
65
+
66
+ ### 5.7 Request-Target Trust Boundary Expansion
67
+ - **Threat:** Applications may pass untrusted or insufficiently validated absolute URLs into request paths that can carry OAuth credentials or authenticated state.
68
+ - **Risk:** This can expand trust boundaries, contributing to token leakage, authenticated requests to unintended hosts, or SSRF-like pivoting in the surrounding application.
69
+ - **Mitigations:**
70
+ - Prefer relative paths where practical
71
+ - Do not pass untrusted absolute URLs into token-bearing clients
72
+ - Validate or allowlist outbound request targets at the application layer
73
+ - Treat request-target validation as a separate concern from log redaction and token storage
74
+
75
+ ### 5.8 Insufficient Logging and Monitoring
76
+ - **Threat:** Attacks go undetected
77
+ - **Mitigations:**
78
+ - Log security-relevant events (without sensitive data)
79
+ - Monitor for suspicious activity
80
+
81
+ ## 6. Assumptions
82
+ - The gem is used in a secure environment with up-to-date Ruby and dependencies
83
+ - End-users are responsible for secure configuration and deployment
84
+
85
+ ## 7. Out of Scope
86
+ - Security of external OAuth providers
87
+ - Application-level business logic
88
+
89
+ ## 8. References
90
+ - [OAuth 2.0 Threat Model and Security Considerations (RFC 6819)](https://tools.ietf.org/html/rfc6819)
91
+ - [OWASP Top Ten](https://owasp.org/www-project-top-ten/)
92
+
93
+ ---
94
+ This outline should be reviewed and updated regularly as the project evolves.
@@ -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)
@@ -134,7 +131,7 @@ You may need to set `snaky: false`. See inline documentation for more info.
134
131
  # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
135
132
  # @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
136
133
  # either one of :header, :body or :query; or a Hash with verb symbols as keys mapping to one of these symbols
137
- # (e.g., {get: :query, post: :header, delete: :header}); or a callable that accepts a request-verb parameter
134
+ # (e.g., `{get: :query, post: :header, delete: :header}`); or a callable that accepts a request-verb parameter
138
135
  # and returns one of these three symbols.
139
136
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
140
137
  #
@@ -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 {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
  #
@@ -563,7 +563,15 @@ module OAuth2
563
563
  end
564
564
 
565
565
  def oauth_debug_logging(builder)
566
- builder.response(:logger, options[:logger], bodies: true) if OAuth2::OAUTH_DEBUG
566
+ builder.response(
567
+ :logger,
568
+ Auth::Sanitizer::SanitizedLogger.new(
569
+ options[:logger],
570
+ filtered_keys: OAuth2.config[:filtered_debug_keys],
571
+ label: OAuth2.config[:filtered_label],
572
+ ),
573
+ bodies: true,
574
+ ) if OAuth2::OAUTH_DEBUG
567
575
  end
568
576
  end
569
577
  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,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module OAuth2
2
- # Mixin that redacts sensitive instance variables in #inspect output.
4
+ # Permanent alias for {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
+ #
10
+ # New code that does not need the `OAuth2::` namespace can use
11
+ # {Auth::Sanitizer::FilteredAttributes} directly.
12
+ FilteredAttributes = Auth::Sanitizer::FilteredAttributes
52
13
  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
 
@@ -66,8 +66,8 @@ module OAuth2
66
66
  # @see https://datatracker.ietf.org/doc/html/rfc7518#section-3.1
67
67
  #
68
68
  # The object type of `:key` may depend on the value of `:algorithm`. Sample arguments:
69
- # get_token(claim_set, {:algorithm => 'HS256', :key => 'secret_key'})
70
- # get_token(claim_set, {:algorithm => 'RS256', :key => OpenSSL::PKCS12.new(File.read('my_key.p12'), 'not_secret')})
69
+ # `get_token(claim_set, {:algorithm => 'HS256', :key => 'secret_key'})`
70
+ # `get_token(claim_set, {:algorithm => 'RS256', :key => OpenSSL::PKCS12.new(File.read('my_key.p12'), 'not_secret')})`
71
71
  #
72
72
  # @param [Hash] request_opts options that will be used to assemble the request
73
73
  # @option request_opts [String] :scope the url parameter `scope` that may be required by some endpoints
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OAuth2
4
4
  module Version
5
- VERSION = "2.0.17"
5
+ VERSION = "2.0.19"
6
6
  end
7
7
  end
data/lib/oauth2.rb CHANGED
@@ -1,10 +1,11 @@
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
8
+ require "auth/sanitizer"
8
9
  require "snaky_hash"
9
10
  require "version_gem"
10
11
 
@@ -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 Auth::Sanitizer's label provider to read from OAuth2.config so that
96
+ # FilteredAttributes-bearing objects and Auth::Sanitizer::SanitizedLogger instances
97
+ # pick up OAuth2.config[:filtered_label] at their initialization time.
98
+ 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
File without changes
File without changes
File without changes
data/sig/oauth2/error.rbs CHANGED
File without changes
@@ -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
File without changes
@@ -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
File without changes
@@ -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
File without changes
data/sig/oauth2.rbs CHANGED
File without changes
data.tar.gz.sig CHANGED
Binary file