Skip to content

Commit 217b7a8

Browse files
pbolingautobolt
andcommitted
🔒️ Redact sensitive values from debug logging output
- Add `OAuth2.config[:filtered_label]` to configure the placeholder used for filtered sensitive values in inspected objects and debug logging output. - Add `OAuth2.config[:filtered_debug_keys]` to configure which key names have their values redacted from debug logging output. - Add `OAuth2::ThingFilter` as the shared filtering primitive used by inspect-time and debug-log filtering. - Make inspect-time and debug-log filters snapshot their configuration at initialization time rather than tracking later config changes. - Automatically redacted values include: - Authorization headers - common token/secret fields in headers - query strings - form bodies - JSON payloads - NOTE: debug logging has always been, and remains, opt-in. It is turned off by default. Co-authored-by: autobolt <autobots@9thbit.net>
1 parent 31bc13c commit 217b7a8

13 files changed

Lines changed: 657 additions & 16 deletions

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ Please file a bug if you notice a violation of semantic versioning.
2020

2121
### Added
2222

23+
- Add `OAuth2.config[:filtered_label]` to configure the placeholder used for filtered sensitive values in inspected objects and debug logging output.
24+
- Add `OAuth2.config[:filtered_debug_keys]` to configure which key names have their values redacted from debug logging output.
25+
- Add `OAuth2::ThingFilter` as the shared filtering primitive used by inspect-time and debug-log filtering.
26+
2327
### Changed
2428

29+
- Make inspect-time and debug-log filters snapshot their configuration at initialization time rather than tracking later config changes.
30+
2531
### Deprecated
2632

2733
### Removed
@@ -30,6 +36,9 @@ Please file a bug if you notice a violation of semantic versioning.
3036

3137
### Security
3238

39+
- Redact sensitive values from debug logging output, including Authorization headers and common token/secret fields in headers, query strings, form bodies, and JSON payloads.
40+
- NOTE: debug logging has always been, and remains, opt-in. It is turned off by defualt.
41+
3342
## [2.0.18] - 2025-11-08
3443

3544
- TAG: [v2.0.18][2.0.18t]

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,19 @@ OAuth2.configure do |config|
330330
end
331331
```
332332

333+
Filtering-related settings:
334+
335+
```ruby
336+
OAuth2.configure do |config|
337+
config.filtered_label = "[REDACTED]" # default: "[FILTERED]"
338+
config.filtered_debug_keys += ["client_assertion"]
339+
end
340+
```
341+
342+
- `filtered_label` controls the placeholder used when sensitive values are filtered from inspected objects and debug logging output.
343+
- `filtered_debug_keys` controls which key names have their values redacted from debug logging output when `OAUTH_DEBUG=true`.
344+
- Debug logging remains opt-in and should still be used cautiously in production environments.
345+
333346
## 🔧 Basic Usage
334347

335348
### Client Initialization Options

lib/oauth2.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010

1111
# includes gem files
1212
require_relative "oauth2/version"
13+
require_relative "oauth2/thing_filter"
1314
require_relative "oauth2/filtered_attributes"
15+
require_relative "oauth2/sanitized_logger"
1416
require_relative "oauth2/error"
1517
require_relative "oauth2/authenticator"
1618
require_relative "oauth2/client"
@@ -43,10 +45,30 @@ module OAuth2
4345
# config[:silence_no_tokens_warning] = false
4446
# end
4547
#
48+
# @example Customize filtered output markers and debug-log value filtering by key name
49+
# OAuth2.configure do |config|
50+
# config[:filtered_label] = "[REDACTED]"
51+
# config[:filtered_debug_keys] += ["client_assertion"]
52+
# end
53+
#
54+
# Existing objects and logger wrappers snapshot filtering configuration during
55+
# initialization. Changing these config values later affects only newly
56+
# initialized objects and debug loggers.
57+
#
4658
# @return [SnakyHash::SymbolKeyed] A mutable Hash-like config with symbol keys
4759
DEFAULT_CONFIG = SnakyHash::SymbolKeyed.new(
4860
silence_extra_tokens_warning: true,
4961
silence_no_tokens_warning: true,
62+
filtered_label: "[FILTERED]",
63+
filtered_debug_keys: %w[
64+
access_token
65+
refresh_token
66+
id_token
67+
client_secret
68+
assertion
69+
code_verifier
70+
token
71+
],
5072
)
5173

5274
# The current runtime configuration for the library.

lib/oauth2/client.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class Client # rubocop:disable Metrics/ClassLength
4242
# @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday
4343
# @option options [Boolean] :raise_errors (true) whether to raise an OAuth2::Error on responses with 400+ status codes
4444
# @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 a {ThingFilter} initialized from `OAuth2.config[:filtered_label]` and the key names in `OAuth2.config[:filtered_debug_keys]`.
4646
# @option options [Class] :access_token_class (AccessToken) class to use for access tokens; you can subclass OAuth2::AccessToken, @version 2.0+
4747
# @option options [Hash] :ssl SSL options for Faraday
4848
#
@@ -563,7 +563,7 @@ def build_access_token_legacy(response, access_token_opts, extract_access_token)
563563
end
564564

565565
def oauth_debug_logging(builder)
566-
builder.response(:logger, options[:logger], bodies: true) if OAuth2::OAUTH_DEBUG
566+
builder.response(:logger, SanitizedLogger.new(options[:logger]), bodies: true) if OAuth2::OAUTH_DEBUG
567567
end
568568
end
569569
end

lib/oauth2/filtered_attributes.rb

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,38 @@
1+
# frozen_string_literal: true
2+
13
module OAuth2
2-
# Mixin that redacts sensitive instance variables in #inspect output.
4+
# Mixin that redacts sensitive instance variables in `#inspect` output.
5+
#
6+
# Classes include this module and declare which attribute names should be
7+
# filtered via {.filtered_attributes}. Matching and replacement behavior is
8+
# delegated to {ThingFilter}, which is initialized once per object.
39
#
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.
10+
# This means existing objects keep the filter configuration that was present
11+
# when they were initialized, even if global config or class-level filter
12+
# declarations change later.
713
module FilteredAttributes
814
# Hook invoked when the module is included. Extends the including class with
9-
# class-level helpers.
15+
# class-level helpers and prepends the initializer hook.
1016
#
1117
# @param [Class] base The including class
1218
# @return [void]
1319
def self.included(base)
1420
base.extend(ClassMethods)
21+
base.prepend(InitializerMethods)
22+
end
23+
24+
# Initializer hook that snapshots the thing filter for this object.
25+
#
26+
# The snapshot captures both the class-level filtered attribute names and
27+
# the current `OAuth2.config[:filtered_label]` value.
28+
module InitializerMethods
29+
def initialize(*args, &block)
30+
super(*args, &block)
31+
@thing_filter = ThingFilter.new(
32+
self.class.filtered_attribute_names,
33+
label: OAuth2.config[:filtered_label],
34+
)
35+
end
1536
end
1637

1738
# Class-level helpers for configuring filtered attributes.
@@ -50,16 +71,24 @@ def filtered_attribute_names
5071
end
5172
end
5273

74+
# The initialized thing filter used by this object.
75+
#
76+
# This is a per-instance snapshot created during initialization.
77+
#
78+
# @return [ThingFilter]
79+
def thing_filter
80+
@thing_filter
81+
end
82+
5383
# Custom inspect that redacts configured attributes.
5484
#
5585
# @return [String]
5686
def inspect
57-
filtered_attribute_names = ClassMethods.filtered_attribute_names(self.class)
58-
return super if filtered_attribute_names.empty?
87+
return super if thing_filter.things.empty?
5988

6089
inspected_vars = instance_variables.map do |var|
61-
if filtered_attribute_names.any? { |filtered_var| var.to_s.include?(filtered_var.to_s) }
62-
"#{var}=[FILTERED]"
90+
if thing_filter.filtered?(var)
91+
"#{var}=#{thing_filter.label}"
6392
else
6493
"#{var}=#{instance_variable_get(var).inspect}"
6594
end

0 commit comments

Comments
 (0)