oauth2 2.0.24 → 2.0.26

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.
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+
5
+ begin
6
+ require "faraday/logging/formatter"
7
+ rescue LoadError
8
+ # Faraday 0.x has no pluggable logging formatter; the text sanitizer remains
9
+ # active through OAuth2::AUTH_SANITIZER::SanitizedLogger.
10
+ end
11
+
12
+ module OAuth2
13
+ if defined?(Faraday::Logging::Formatter) # Faraday >= 1.0
14
+ # Faraday logging formatter that redacts sensitive values from Hash and Array
15
+ # request/response bodies BEFORE Faraday renders them (via `pretty_inspect`).
16
+ #
17
+ # Text-level sanitization ({OAuth2::AUTH_SANITIZER::SanitizedLogger}) remains as a
18
+ # second layer for headers, query strings, and already-serialized bodies.
19
+ #
20
+ # Redaction rules:
21
+ # - keys matching any filtered key (case-insensitive) are redacted, nested at any depth
22
+ # - the authorization `code` is redacted only in an authorization_code grant context,
23
+ # detected from the request URL query or the body itself, so non-secret codes
24
+ # (e.g. error codes) are preserved
25
+ # - cyclic structures are tolerated
26
+ #
27
+ # Requires Faraday >= 1.0 (Faraday::Logging::Formatter). On Faraday 0.17 this
28
+ # class is not defined and the caller falls back to text-level sanitization only.
29
+ #
30
+ # @api private
31
+ class SanitizedLogFormatter < Faraday::Logging::Formatter
32
+ # Keys redacted only in an authorization_code grant context.
33
+ CONTEXTUAL_KEYS = %w[code].freeze
34
+
35
+ # Faraday instantiates formatters itself with (logger:, options:), so the
36
+ # redaction configuration falls back to the current OAuth2.config values.
37
+ #
38
+ # @param filtered_keys [Array<String>, nil] key names to redact unconditionally
39
+ # @param label [String, nil] replacement label
40
+ # @param options [Hash] Faraday formatter options
41
+ def initialize(filtered_keys: nil, label: nil, **options)
42
+ super(**options)
43
+ config = OAuth2.config
44
+ @filtered_keys = (filtered_keys || config[:filtered_debug_keys]).map { |key| key.to_s.downcase }.freeze
45
+ @label = (label || config[:filtered_label]).to_s
46
+ @code_context = false
47
+ end
48
+
49
+ # Log the request, detecting authorization_code context from the URL and body.
50
+ #
51
+ # @param env [Faraday::Env] request environment
52
+ def request(env)
53
+ with_code_context(env) do
54
+ if @code_context && env.respond_to?(:url) && env.url && env.url.query.to_s.include?("code=")
55
+ # Log a sanitized COPY of the env; the real request must keep its URL.
56
+ sanitized_env = env.dup
57
+ sanitized_env.url = sanitize_url_code(env.url)
58
+ super(sanitized_env)
59
+ else
60
+ super
61
+ end
62
+ end
63
+ end
64
+
65
+ # Log the response, detecting authorization_code context from the URL and body.
66
+ #
67
+ # @param env [Faraday::Env] response environment
68
+ def response(env)
69
+ with_code_context(env) { super }
70
+ end
71
+
72
+ # Redact structured bodies before rendering; leave everything else to the parent.
73
+ #
74
+ # @param body [Object] request or response body
75
+ # @return [String] rendered body for logging
76
+ def dump_body(body)
77
+ case body
78
+ when Hash, Array
79
+ super(redact_structured(body, seen: {}, code_sensitive: @code_context))
80
+ when String
81
+ super(@code_context ? redact_code_in_string(body) : body)
82
+ else
83
+ super
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ # Redact the authorization `code` value in URL-encoded, JSON, and
90
+ # Hash#inspect-shaped string bodies. Only called in authorization_code
91
+ # grant context, so `code` always names the credential here.
92
+ def redact_code_in_string(body)
93
+ body.dup.scrub.
94
+ gsub(/\bcode=[^&\s"']+/, "code=#{@label}").
95
+ gsub(/("code"\s*:\s*")[^"]*(")/i) { "#{$1}#{@label}#{$2}" }.
96
+ gsub(/(("code"|code:)\s*=>?\s*")[^"]*(")/i) { "#{$1}#{@label}#{$3}" }
97
+ end
98
+
99
+ # Redact the `code` query parameter of a URL, returning a new URI.
100
+ def sanitize_url_code(url)
101
+ sanitized = url.dup
102
+ pairs = sanitized.query.to_s.tr("+", " ").split("&").map do |pair|
103
+ pair.start_with?("code=") ? "code=#{@label}" : pair
104
+ end
105
+ sanitized.query = pairs.join("&")
106
+ sanitized
107
+ end
108
+
109
+ def with_code_context(env)
110
+ previous = @code_context
111
+ @code_context = previous || authorization_code_request?(env)
112
+ yield
113
+ ensure
114
+ @code_context = previous
115
+ end
116
+
117
+ def authorization_code_request?(env)
118
+ url = env.respond_to?(:url) ? env.url : nil
119
+ if url
120
+ query_pairs = url.query.to_s.tr("+", " ").split("&")
121
+ return true if query_pairs.any? { |pair| pair == "grant_type=authorization_code" }
122
+ end
123
+
124
+ body = env.respond_to?(:body) ? env.body : nil
125
+ case body
126
+ when String
127
+ scrubbed = body.dup.scrub
128
+ scrubbed.match?(/grant_type=authorization_code/) ||
129
+ scrubbed.match?(/grant(?:\\u005f|_)type\\?"?\s*:\s*\\?"authorization_code/)
130
+ else
131
+ authorization_code_context?(body) || authorization_code_pairs_context?(body)
132
+ end
133
+ end
134
+
135
+ def redact_structured(node, seen:, code_sensitive:)
136
+ return node if node.nil?
137
+
138
+ case node
139
+ when Hash
140
+ object_id = node.object_id
141
+ return seen[object_id] if seen.key?(object_id)
142
+
143
+ copy = {}
144
+ seen[object_id] = copy
145
+ code_sensitive ||= authorization_code_context?(node)
146
+ node.each do |key, value|
147
+ copy[key] = if redact_key?(key, redact_code: code_sensitive)
148
+ @label
149
+ else
150
+ redact_structured(value, seen: seen, code_sensitive: code_sensitive)
151
+ end
152
+ end
153
+ copy
154
+ when Array
155
+ object_id = node.object_id
156
+ return seen[object_id] if seen.key?(object_id)
157
+
158
+ copy = []
159
+ seen[object_id] = copy
160
+ code_sensitive ||= authorization_code_pairs_context?(node)
161
+ if pair_array?(node)
162
+ node.each do |pair|
163
+ copy << if redact_key?(pair[0], redact_code: code_sensitive)
164
+ [pair[0], @label]
165
+ else
166
+ [pair[0], redact_structured(pair[1], seen: seen, code_sensitive: code_sensitive)]
167
+ end
168
+ end
169
+ else
170
+ node.each { |element| copy << redact_structured(element, seen: seen, code_sensitive: code_sensitive) }
171
+ end
172
+ copy
173
+ else
174
+ node
175
+ end
176
+ end
177
+
178
+ def redact_key?(key, redact_code:)
179
+ name = key.to_s.downcase
180
+ return true if @filtered_keys.include?(name)
181
+
182
+ redact_code && CONTEXTUAL_KEYS.include?(name)
183
+ end
184
+
185
+ def authorization_code_context?(node)
186
+ node.is_a?(Hash) && node.any? { |key, value| key.to_s == "grant_type" && value.to_s == "authorization_code" }
187
+ end
188
+
189
+ def pair_array?(node)
190
+ node.is_a?(Array) && !node.empty? && node.all? { |element| element.is_a?(Array) && element.length == 2 }
191
+ end
192
+
193
+ def authorization_code_pairs_context?(node)
194
+ pair_array?(node) && node.any? { |pair| pair[0].to_s == "grant_type" && pair[1].to_s == "authorization_code" }
195
+ end
196
+ end
197
+ end
198
+ end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OAuth2
4
+ # Version namespace for this gem.
4
5
  module Version
5
- VERSION = "2.0.24"
6
+ # Current gem version.
7
+ VERSION = "2.0.26"
6
8
  end
9
+ # Current gem version exposed at the traditional constant location.
7
10
  VERSION = Version::VERSION # Traditional Constant Location
8
11
  end
data/lib/oauth2.rb CHANGED
@@ -9,9 +9,9 @@ require "snaky_hash"
9
9
  require "version_gem"
10
10
 
11
11
  # includes gem files
12
- require_relative "oauth2/version"
13
12
  require_relative "oauth2/auth_sanitizer"
14
13
  require_relative "oauth2/filtered_attributes"
14
+ require_relative "oauth2/sanitized_log_formatter"
15
15
  require_relative "oauth2/error"
16
16
  require_relative "oauth2/authenticator"
17
17
  require_relative "oauth2/client"
@@ -23,6 +23,7 @@ require_relative "oauth2/strategy/client_credentials"
23
23
  require_relative "oauth2/strategy/assertion"
24
24
  require_relative "oauth2/access_token"
25
25
  require_relative "oauth2/response"
26
+ require_relative "oauth2/version"
26
27
 
27
28
  # The namespace of this library
28
29
  #
@@ -67,6 +68,14 @@ module OAuth2
67
68
  assertion
68
69
  code_verifier
69
70
  token
71
+ password
72
+ client_assertion
73
+ device_code
74
+ subject_token
75
+ actor_token
76
+ accessToken
77
+ refreshToken
78
+ idToken
70
79
  ]
71
80
  )
72
81
 
@@ -98,6 +107,7 @@ end
98
107
  OAuth2::AUTH_SANITIZER.filtered_label_provider = -> { OAuth2.config[:filtered_label] }
99
108
 
100
109
  # Extend OAuth2::Version with VersionGem helpers to provide semantic version helpers.
110
+
101
111
  OAuth2::Version.class_eval do
102
112
  extend VersionGem::Basic
103
113
  end
data/sig/oauth2.rbs CHANGED
@@ -6,4 +6,210 @@ module OAuth2
6
6
 
7
7
  def self.config: () -> untyped
8
8
  def self.configure: () { (untyped) -> void } -> void
9
+ module Version
10
+ VERSION: String
11
+ end
12
+ VERSION: String
13
+ class AccessToken
14
+ def self.from_hash: (OAuth2::Client, Hash[untyped, untyped]) -> OAuth2::AccessToken
15
+ def self.from_kvform: (OAuth2::Client, String) -> OAuth2::AccessToken
16
+
17
+ def initialize: (OAuth2::Client, String, ?Hash[Symbol, untyped]) -> void
18
+ def []: (String | Symbol) -> untyped
19
+ def expires?: () -> bool
20
+ def expired?: () -> bool
21
+ def refresh: (?Hash[untyped, untyped], ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::AccessToken
22
+ def revoke: (?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
23
+ def to_hash: () -> Hash[Symbol, untyped]
24
+ def request: (Symbol, String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
25
+ def get: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
26
+ def post: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
27
+ def put: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
28
+ def patch: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
29
+ def delete: (String, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
30
+ def headers: () -> Hash[String, String]
31
+ def configure_authentication!: (Hash[Symbol, untyped], Symbol) -> void
32
+ def convert_expires_at: (untyped) -> (Time | Integer | nil)
33
+
34
+ attr_accessor response: OAuth2::Response
35
+ end
36
+
37
+ class Authenticator
38
+ include OAuth2::FilteredAttributes
39
+
40
+ attr_reader mode: (Symbol | String)
41
+ attr_reader id: String?
42
+ attr_reader secret: String?
43
+
44
+ def initialize: (String? id, String? secret, (Symbol | String) mode) -> void
45
+
46
+ def apply: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
47
+
48
+ def self.encode_basic_auth: (String, String) -> String
49
+
50
+ private
51
+
52
+ def apply_params_auth: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
53
+ def apply_client_id: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
54
+ def apply_basic_auth: (Hash[untyped, untyped]) -> Hash[untyped, untyped]
55
+ def basic_auth_header: () -> Hash[String, String]
56
+ end
57
+
58
+ class Client
59
+ RESERVED_REQ_KEYS: Array[String]
60
+ RESERVED_PARAM_KEYS: Array[String]
61
+
62
+ include OAuth2::FilteredAttributes
63
+
64
+ attr_reader id: String
65
+ attr_reader secret: String
66
+ attr_reader site: String?
67
+ attr_accessor options: Hash[Symbol, untyped]
68
+ attr_writer connection: untyped
69
+
70
+ def initialize: (String client_id, String client_secret, ?Hash[Symbol, untyped]) { (untyped) -> void } -> void
71
+
72
+ def site=: (String) -> String
73
+
74
+ def connection: () -> untyped
75
+
76
+ def authorize_url: (?Hash[untyped, untyped]) -> String
77
+ def token_url: (?Hash[untyped, untyped]) -> String
78
+ def revoke_url: (?Hash[untyped, untyped]) -> String
79
+
80
+ def request: (Symbol verb, String url, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
81
+
82
+ def get_token: (Hash[untyped, untyped] params, ?Hash[Symbol, untyped] access_token_opts, ?Proc) { (Hash[Symbol, untyped]) -> void } -> (OAuth2::AccessToken | nil)
83
+
84
+ def revoke_token: (String token, ?String token_type_hint, ?Hash[Symbol, untyped]) { (untyped) -> void } -> OAuth2::Response
85
+
86
+ def http_method: () -> Symbol
87
+
88
+ def auth_code: () -> OAuth2::Strategy::AuthCode
89
+ def implicit: () -> OAuth2::Strategy::Implicit
90
+ def password: () -> OAuth2::Strategy::Password
91
+ def client_credentials: () -> OAuth2::Strategy::ClientCredentials
92
+ def assertion: () -> OAuth2::Strategy::Assertion
93
+
94
+ def redirection_params: () -> Hash[String, String]
95
+
96
+ private
97
+
98
+ def params_to_req_opts: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
99
+ def parse_snaky_params_headers: (Hash[untyped, untyped]) -> [Symbol, bool, untyped, (Symbol | nil), Hash[untyped, untyped], Hash[String, String]]
100
+ def execute_request: (Symbol verb, String url, ?Hash[Symbol, untyped]) { (Faraday::Request) -> void } -> OAuth2::Response
101
+ def authenticator: () -> OAuth2::Authenticator
102
+ def parse_response_legacy: (OAuth2::Response, Hash[Symbol, untyped], Proc) -> (OAuth2::AccessToken | nil)
103
+ def parse_response: (OAuth2::Response, Hash[Symbol, untyped]) -> (OAuth2::AccessToken | nil)
104
+ def build_access_token: (OAuth2::Response, Hash[Symbol, untyped], untyped) -> OAuth2::AccessToken
105
+ def build_access_token_legacy: (OAuth2::Response, Hash[Symbol, untyped], Proc) -> (OAuth2::AccessToken | nil)
106
+ def oauth_debug_logging: (untyped) -> void
107
+ end
108
+
109
+ class Error < StandardError
110
+ def initialize: (OAuth2::Response) -> void
111
+ def code: () -> (String | Integer | nil)
112
+ def description: () -> (String | nil)
113
+ def response: () -> OAuth2::Response
114
+ end
115
+
116
+ module FilteredAttributes
117
+ module InitializerMethods
118
+ def initialize: (*untyped args) { () -> untyped } -> untyped
119
+ end
120
+
121
+ def self.included: (untyped) -> untyped
122
+ def filtered_attributes: (*(String | Symbol)) -> void
123
+ def thing_filter: () -> OAuth2::ThingFilter
124
+ end
125
+
126
+ class Response
127
+ DEFAULT_OPTIONS: Hash[Symbol, untyped]
128
+
129
+ def self.register_parser: (Symbol key, (Array[String] | String) mime_types) { (String) -> untyped } -> void
130
+
131
+ def initialize: (untyped response, parse: Symbol?, snaky: bool?, snaky_hash_klass: untyped?, options: Hash[Symbol, untyped]?) -> void
132
+ def headers: () -> Hash[untyped, untyped]
133
+ def status: () -> Integer
134
+ def body: () -> String
135
+ def parsed: () -> untyped
136
+ def content_type: () -> (String | nil)
137
+ def parser: () -> (untyped | nil)
138
+
139
+ attr_reader response: untyped
140
+ attr_accessor options: Hash[Symbol, untyped]
141
+ end
142
+
143
+ class SanitizedLogger
144
+ def initialize: (untyped logger) -> void
145
+
146
+ def add: (untyped severity, ?untyped message, ?untyped progname) { () -> untyped } -> untyped
147
+ def <<: (String message) -> untyped
148
+ def debug: (?untyped progname) { () -> untyped } -> untyped
149
+ def info: (?untyped progname) { () -> untyped } -> untyped
150
+ def warn: (?untyped progname) { () -> untyped } -> untyped
151
+ def error: (?untyped progname) { () -> untyped } -> untyped
152
+ def fatal: (?untyped progname) { () -> untyped } -> untyped
153
+ def unknown: (?untyped progname) { () -> untyped } -> untyped
154
+ def close: () -> void
155
+ def formatter: () -> untyped
156
+ def formatter=: (untyped formatter) -> void
157
+ def level: () -> untyped
158
+ def level=: (untyped level) -> void
159
+ def progname: () -> untyped
160
+ def progname=: (untyped progname) -> void
161
+ def respond_to_missing?: (Symbol method_name, ?bool include_private) -> bool
162
+ def method_missing: (Symbol method_name, *untyped args) { () -> untyped } -> untyped
163
+
164
+ private
165
+
166
+ def log: (Symbol level, ?untyped progname) { () -> untyped } -> untyped
167
+ def sanitize: (untyped message) -> untyped
168
+ def thing_filter: () -> OAuth2::ThingFilter
169
+ def sanitize_authorization_header: (String message) -> String
170
+ def sanitize_json_pairs: (String message) -> String
171
+ def sanitize_form_and_query_pairs: (String message) -> String
172
+ end
173
+
174
+ module Strategy
175
+ class Base
176
+ def initialize: (OAuth2::Client) -> void
177
+ end
178
+
179
+ class AuthCode < Base
180
+ def authorize_params: (?Hash[untyped, untyped]) -> Hash[untyped, untyped]
181
+ def authorize_url: (?Hash[untyped, untyped]) -> String
182
+ def get_token: (String, ?Hash[untyped, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
183
+ end
184
+
185
+ class Implicit < Base
186
+ def authorize_params: (?Hash[untyped, untyped]) -> Hash[untyped, untyped]
187
+ def authorize_url: (?Hash[untyped, untyped]) -> String
188
+ def get_token: (*untyped) -> void
189
+ end
190
+
191
+ class Password < Base
192
+ def authorize_url: () -> void
193
+ def get_token: (String, String, ?Hash[untyped, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
194
+ end
195
+
196
+ class ClientCredentials < Base
197
+ def authorize_url: () -> void
198
+ def get_token: (?Hash[untyped, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
199
+ end
200
+
201
+ class Assertion < Base
202
+ def authorize_url: () -> void
203
+ def get_token: (Hash[untyped, untyped], Hash[Symbol, untyped], ?Hash[Symbol, untyped], ?Hash[Symbol, untyped]) -> OAuth2::AccessToken
204
+ end
205
+ end
206
+
207
+ class ThingFilter
208
+ attr_reader things: Array[String]
209
+ attr_reader label: String
210
+
211
+ def initialize: (Enumerable[untyped] things, label: String) -> void
212
+ def filtered?: (untyped thing_name) -> bool
213
+ def pattern_source: () -> String
214
+ end
9
215
  end
data.tar.gz.sig CHANGED
Binary file