oauth2 2.0.10 → 2.0.17

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 ADDED
File without changes
data/RUBOCOP.md ADDED
@@ -0,0 +1,71 @@
1
+ # RuboCop Usage Guide
2
+
3
+ ## Overview
4
+
5
+ A tale of two RuboCop plugin gems.
6
+
7
+ ### RuboCop Gradual
8
+
9
+ This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
10
+
11
+ ### RuboCop LTS
12
+
13
+ This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
14
+ RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
15
+
16
+ ## Checking RuboCop Violations
17
+
18
+ To check for RuboCop violations in this project, always use:
19
+
20
+ ```bash
21
+ bundle exec rake rubocop_gradual:check
22
+ ```
23
+
24
+ **Do not use** the standard RuboCop commands like:
25
+ - `bundle exec rubocop`
26
+ - `rubocop`
27
+
28
+ ## Understanding the Lock File
29
+
30
+ The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
31
+
32
+ 1. Prevent new violations while gradually fixing existing ones
33
+ 2. Track progress on code style improvements
34
+ 3. Ensure CI builds don't fail due to pre-existing violations
35
+
36
+ ## Common Commands
37
+
38
+ - **Check violations**
39
+ - `bundle exec rake rubocop_gradual`
40
+ - `bundle exec rake rubocop_gradual:check`
41
+ - **(Safe) Autocorrect violations, and update lockfile if no new violations**
42
+ - `bundle exec rake rubocop_gradual:autocorrect`
43
+ - **Force update the lock file (w/o autocorrect) to match violations present in code**
44
+ - `bundle exec rake rubocop_gradual:force_update`
45
+
46
+ ## Workflow
47
+
48
+ 1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
49
+ a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
50
+ 2. If there are new violations, either:
51
+ - Fix them in your code
52
+ - Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
53
+ 3. Commit the updated `.rubocop_gradual.lock` file along with your changes
54
+
55
+ ## Never add inline RuboCop disables
56
+
57
+ Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
58
+
59
+ - Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
60
+ - Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
61
+ - `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
62
+ - If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
63
+
64
+ In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
65
+
66
+ ## Benefits of rubocop_gradual
67
+
68
+ - Allows incremental adoption of code style rules
69
+ - Prevents CI failures due to pre-existing violations
70
+ - Provides a clear record of code style debt
71
+ - Enables focused efforts on improving code quality over time
data/SECURITY.md CHANGED
@@ -2,25 +2,20 @@
2
2
 
3
3
  ## Supported Versions
4
4
 
5
- | Version | Supported | EOL | Post-EOL / Enterprise |
6
- |----------|-----------|---------|---------------------------------------|
7
- | 2.latest | ✅ | 04/2026 | [Tidelift Subscription][tidelift-ref] |
8
- | 1.latest | ✅ | 10/2025 | [Tidelift Subscription][tidelift-ref] |
9
- | <= 1 | ⛔ | ⛔ | ⛔ |
5
+ | Version | Supported |
6
+ |----------|-----------|
7
+ | 1.latest | ✅ |
10
8
 
11
- ### EOL Policy
9
+ ## Security contact information
12
10
 
13
- Non-commercial support for the oldest version of Ruby (which itself is going EOL) will be dropped each year in April.
14
-
15
- ## Reporting a Vulnerability
16
-
17
- To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
11
+ To report a security vulnerability, please use the
12
+ [Tidelift security contact](https://tidelift.com/security).
18
13
  Tidelift will coordinate the fix and disclosure.
19
14
 
20
- ## OAuth2 for Enterprise
21
-
22
- Available as part of the Tidelift Subscription.
15
+ ## Additional Support
23
16
 
24
- The maintainers of oauth2 and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. [Learn more.][tidelift-ref]
17
+ If you are interested in support for versions older than the latest release,
18
+ please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
19
+ or find other sponsorship links in the [README].
25
20
 
26
- [tidelift-ref]: https://tidelift.com/subscription/pkg/rubygems-oauth2?utm_source=rubygems-oauth2&utm_medium=referral&utm_campaign=enterprise&utm_term=repo
21
+ [README]: README.md
@@ -1,5 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :nocov:
4
+ begin
5
+ # The first version of hashie that has a version file was 1.1.0
6
+ # The first version of hashie that required the version file at runtime was 3.2.0
7
+ # If it has already been loaded then this is very low cost, as Kernel.require uses maintains a cache
8
+ # If this it hasn't this will work to get it loaded, and then we will be able to use
9
+ # defined?(Hashie::Version)
10
+ # as a test.
11
+ # TODO: get rid this mess when we drop Hashie < 3.2, as Hashie will self-load its version then
12
+ require "hashie/version"
13
+ rescue LoadError
14
+ nil
15
+ end
16
+ # :nocov:
17
+
3
18
  module OAuth2
4
19
  class AccessToken # rubocop:disable Metrics/ClassLength
5
20
  TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
@@ -54,7 +69,16 @@ module OAuth2
54
69
  extra_tokens_warning(supported_keys, t_key)
55
70
  t_key
56
71
  end
57
- token = fresh.delete(key) || ""
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] || ""
78
+ else
79
+ fresh.delete(key) || ""
80
+ end
81
+ # :nocov:
58
82
  new(client, token, fresh)
59
83
  end
60
84
 
@@ -108,9 +132,15 @@ You may need to set `snaky: false`. See inline documentation for more info.
108
132
  # @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
109
133
  # @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
110
134
  # @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
111
- # @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
112
- # one of :header, :body or :query
135
+ # @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
136
+ # 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
138
+ # and returns one of these three symbols.
113
139
  # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
140
+ #
141
+ # @example Verb-dependent Hash mode
142
+ # # Send token in query for GET, in header for POST/DELETE, in body for PUT/PATCH
143
+ # OAuth2::AccessToken.new(client, token, mode: {get: :query, post: :header, delete: :header, put: :body, patch: :body})
114
144
  # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
115
145
  # Access Token value in :body or :query transmission mode
116
146
  # @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
@@ -300,7 +330,7 @@ You may need to set `snaky: false`. See inline documentation for more info.
300
330
  #
301
331
  # @see OAuth2::Client#request
302
332
  def request(verb, path, opts = {}, &block)
303
- configure_authentication!(opts)
333
+ configure_authentication!(opts, verb)
304
334
  @client.request(verb, path, opts, &block)
305
335
  end
306
336
 
@@ -346,12 +376,26 @@ You may need to set `snaky: false`. See inline documentation for more info.
346
376
 
347
377
  private
348
378
 
349
- def configure_authentication!(opts)
350
- case options[:mode]
379
+ def configure_authentication!(opts, verb)
380
+ mode_opt = options[:mode]
381
+ mode =
382
+ if mode_opt.respond_to?(:call)
383
+ mode_opt.call(verb)
384
+ elsif mode_opt.is_a?(Hash)
385
+ key = verb.to_sym
386
+ # Try symbol key first, then string key; default to :header when missing
387
+ mode_opt[key] || mode_opt[key.to_s] || :header
388
+ else
389
+ mode_opt
390
+ end
391
+
392
+ case mode
351
393
  when :header
352
394
  opts[:headers] ||= {}
353
395
  opts[:headers].merge!(headers)
354
396
  when :query
397
+ # OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
398
+ # Prefer the default :header mode whenever possible.
355
399
  opts[:params] ||= {}
356
400
  opts[:params][options[:param_name]] = token
357
401
  when :body
@@ -363,7 +407,7 @@ You may need to set `snaky: false`. See inline documentation for more info.
363
407
  end
364
408
  # @todo support for multi-part (file uploads)
365
409
  else
366
- raise("invalid :mode option of #{options[:mode]}")
410
+ raise("invalid :mode option of #{mode}")
367
411
  end
368
412
  end
369
413
 
@@ -3,12 +3,24 @@
3
3
  require "base64"
4
4
 
5
5
  module OAuth2
6
+ # Builds and applies client authentication to token and revoke requests.
7
+ #
8
+ # Depending on the selected mode, credentials are applied as Basic Auth
9
+ # headers, request body parameters, or only the client_id is sent (TLS).
6
10
  class Authenticator
7
11
  include FilteredAttributes
8
12
 
13
+ # @return [Symbol, String] Authentication mode (e.g., :basic_auth, :request_body, :tls_client_auth, :private_key_jwt)
14
+ # @return [String, nil] Client identifier
15
+ # @return [String, nil] Client secret (filtered in inspected output)
9
16
  attr_reader :mode, :id, :secret
10
17
  filtered_attributes :secret
11
18
 
19
+ # Create a new Authenticator
20
+ #
21
+ # @param [String, nil] id Client identifier
22
+ # @param [String, nil] secret Client secret
23
+ # @param [Symbol, String] mode Authentication mode
12
24
  def initialize(id, secret, mode)
13
25
  @id = id
14
26
  @secret = secret
@@ -39,6 +51,11 @@ module OAuth2
39
51
  end
40
52
  end
41
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
42
59
  def self.encode_basic_auth(user, password)
43
60
  "Basic #{Base64.strict_encode64("#{user}:#{password}")}"
44
61
  end
@@ -47,6 +64,9 @@ module OAuth2
47
64
 
48
65
  # Adds client_id and client_secret request parameters if they are not
49
66
  # already set.
67
+ #
68
+ # @param [Hash] params Request parameters
69
+ # @return [Hash] Updated parameters including client_id and client_secret
50
70
  def apply_params_auth(params)
51
71
  result = {}
52
72
  result["client_id"] = id unless id.nil?
@@ -54,8 +74,11 @@ module OAuth2
54
74
  result.merge(params)
55
75
  end
56
76
 
57
- # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
77
+ # When using schemes that don't require the client_secret to be passed (e.g., TLS Client Auth),
58
78
  # we don't want to send the secret
79
+ #
80
+ # @param [Hash] params Request parameters
81
+ # @return [Hash] Updated parameters including only client_id
59
82
  def apply_client_id(params)
60
83
  result = {}
61
84
  result["client_id"] = id unless id.nil?
@@ -64,13 +87,19 @@ module OAuth2
64
87
 
65
88
  # Adds an `Authorization` header with Basic Auth credentials if and only if
66
89
  # it is not already set in the params.
90
+ #
91
+ # @param [Hash] params Request parameters (may include :headers)
92
+ # @return [Hash] Updated parameters with Authorization header
67
93
  def apply_basic_auth(params)
68
94
  headers = params.fetch(:headers, {})
69
95
  headers = basic_auth_header.merge(headers)
70
96
  params.merge(headers: headers)
71
97
  end
72
98
 
99
+ # Build the Basic Authorization header.
100
+ #
73
101
  # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
102
+ # @return [Hash] Header hash containing the Authorization entry
74
103
  def basic_auth_header
75
104
  {"Authorization" => self.class.encode_basic_auth(id, secret)}
76
105
  end
data/lib/oauth2/client.rb CHANGED
@@ -19,7 +19,7 @@ module OAuth2
19
19
  # The OAuth2::Client class
20
20
  class Client # rubocop:disable Metrics/ClassLength
21
21
  RESERVED_REQ_KEYS = %w[body headers params redirect_count].freeze
22
- RESERVED_PARAM_KEYS = (RESERVED_REQ_KEYS + %w[parse snaky token_method]).freeze
22
+ RESERVED_PARAM_KEYS = (RESERVED_REQ_KEYS + %w[parse snaky snaky_hash_klass token_method]).freeze
23
23
 
24
24
  include FilteredAttributes
25
25
 
@@ -256,10 +256,10 @@ module OAuth2
256
256
  # @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
257
257
  def revoke_token(token, token_type_hint = nil, params = {}, &block)
258
258
  params[:token_method] ||= :post_with_query_string
259
+ params[:token] = token
260
+ params[:token_type_hint] = token_type_hint if token_type_hint
261
+
259
262
  req_opts = params_to_req_opts(params)
260
- req_opts[:params] ||= {}
261
- req_opts[:params][:token] = token
262
- req_opts[:params][:token_type_hint] = token_type_hint if token_type_hint
263
263
 
264
264
  request(http_method, revoke_url, req_opts, &block)
265
265
  end
@@ -321,6 +321,9 @@ module OAuth2
321
321
  # requesting authorization. If it is provided at authorization time it MUST
322
322
  # also be provided with the token exchange request.
323
323
  #
324
+ # OAuth 2.1 note: Authorization Servers must compare redirect URIs using exact string matching.
325
+ # This client simply forwards the configured redirect_uri; the exact-match validation happens server-side.
326
+ #
324
327
  # Providing :redirect_uri to the OAuth2::Client instantiation will take
325
328
  # care of managing this.
326
329
  #
@@ -330,6 +333,7 @@ module OAuth2
330
333
  # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
331
334
  # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
332
335
  # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
336
+ # @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13
333
337
  #
334
338
  # @return [Hash] the params to add to a request or URL
335
339
  def redirection_params
@@ -342,14 +346,26 @@ module OAuth2
342
346
 
343
347
  private
344
348
 
345
- # A generic token request options parser
349
+ # Processes request parameters and transforms them into request options
350
+ #
351
+ # @param [Hash] params the request parameters to process
352
+ # @option params [Symbol] :parse (:automatic) parsing strategy for the response
353
+ # @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
354
+ # @option params [Class] :snaky_hash_klass (SnakyHash::StringKeyed) class to use for snake_case hash conversion
355
+ # @option params [Symbol] :token_method (:post) HTTP method to use for token request
356
+ # @option params [Hash] :headers Additional HTTP headers for the request
357
+ #
358
+ # @return [Hash] the processed request options
359
+ #
360
+ # @api private
346
361
  def params_to_req_opts(params)
347
- parse, snaky, token_method, params, headers = parse_snaky_params_headers(params)
362
+ parse, snaky, snaky_hash_klass, token_method, params, headers = parse_snaky_params_headers(params)
348
363
  req_opts = {
349
364
  raise_errors: options[:raise_errors],
350
365
  token_method: token_method || options[:token_method],
351
366
  parse: parse,
352
367
  snaky: snaky,
368
+ snaky_hash_klass: snaky_hash_klass,
353
369
  }
354
370
  if req_opts[:token_method] == :post
355
371
  # NOTE: If proliferation of request types continues, we should implement a parser solution for Request,
@@ -369,19 +385,22 @@ module OAuth2
369
385
  req_opts
370
386
  end
371
387
 
372
- # Processes and transforms the input parameters for OAuth requests
388
+ # Processes and transforms parameters for OAuth requests
373
389
  #
374
390
  # @param [Hash] params the input parameters to process
375
- # @option params [Symbol, nil] :parse (:automatic) parsing strategy for the response
391
+ # @option params [Symbol] :parse (:automatic) parsing strategy for the response
376
392
  # @option params [Boolean] :snaky (true) whether to convert response keys to snake_case
393
+ # @option params [Class] :snaky_hash_klass (SnakyHash::StringKeyed) class to use for snake_case hash conversion
394
+ # @option params [Symbol] :token_method overrides the default token method for this request
377
395
  # @option params [Hash] :headers HTTP headers for the request
378
396
  #
379
- # @return [Array<(Symbol, Boolean, Hash, Hash)>] Returns an array containing:
380
- # - [Symbol, nil] parse strategy
381
- # - [Boolean] snaky flag for response key transformation
382
- # - [Symbol, nil] token_method overrides options[:token_method] for a request
383
- # - [Hash] processed parameters
384
- # - [Hash] HTTP headers
397
+ # @return [Array<(Symbol, Boolean, Class, Symbol, Hash, Hash)>] Returns an array containing:
398
+ # - parse strategy (Symbol)
399
+ # - snaky flag for response key transformation (Boolean)
400
+ # - hash class for snake_case conversion (Class)
401
+ # - token method override (Symbol, nil)
402
+ # - processed parameters (Hash)
403
+ # - HTTP headers (Hash)
385
404
  #
386
405
  # @api private
387
406
  def parse_snaky_params_headers(params)
@@ -394,11 +413,12 @@ module OAuth2
394
413
  end.to_h
395
414
  parse = params.key?(:parse) ? params.delete(:parse) : Response::DEFAULT_OPTIONS[:parse]
396
415
  snaky = params.key?(:snaky) ? params.delete(:snaky) : Response::DEFAULT_OPTIONS[:snaky]
416
+ snaky_hash_klass = params.key?(:snaky_hash_klass) ? params.delete(:snaky_hash_klass) : Response::DEFAULT_OPTIONS[:snaky_hash_klass]
397
417
  token_method = params.delete(:token_method) if params.key?(:token_method)
398
418
  params = authenticator.apply(params)
399
419
  # authenticator may add :headers, and we separate them from params here
400
420
  headers = params.delete(:headers) || {}
401
- [parse, snaky, token_method, params, headers]
421
+ [parse, snaky, snaky_hash_klass, token_method, params, headers]
402
422
  end
403
423
 
404
424
  # Executes an HTTP request with error handling and response processing
data/lib/oauth2/error.rb CHANGED
@@ -1,12 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OAuth2
4
+ # Represents an OAuth2 error condition.
5
+ #
6
+ # Wraps details from an OAuth2::Response or Hash payload returned by an
7
+ # authorization server, exposing error code and description per RFC 6749.
4
8
  class Error < StandardError
9
+ # @return [OAuth2::Response, Hash, Object] Original response or payload used to build the error
10
+ # @return [String] Raw body content (if available)
11
+ # @return [String, nil] Error code (e.g., 'invalid_grant')
12
+ # @return [String, nil] Human-readable description for the error
5
13
  attr_reader :response, :body, :code, :description
6
14
 
7
- # standard error codes include:
8
- # 'invalid_request', 'invalid_client', 'invalid_token', 'invalid_grant', 'unsupported_grant_type', 'invalid_scope'
9
- # response might be a Response object, or the response.parsed hash
15
+ # Create a new OAuth2::Error
16
+ #
17
+ # @param [OAuth2::Response, Hash, Object] response A Response or error payload
10
18
  def initialize(response)
11
19
  @response = response
12
20
  if response.respond_to?(:parsed)
@@ -29,6 +37,11 @@ module OAuth2
29
37
 
30
38
  private
31
39
 
40
+ # Builds a multi-line error message including description and raw body.
41
+ #
42
+ # @param [String, #encode] response_body Response body content
43
+ # @param [Hash] opts Options including :error_description
44
+ # @return [String] Message suitable for StandardError
32
45
  def error_message(response_body, opts = {})
33
46
  lines = []
34
47
 
@@ -46,6 +59,11 @@ module OAuth2
46
59
  lines.join("\n")
47
60
  end
48
61
 
62
+ # Formats the OAuth2 error code and description into a single string.
63
+ #
64
+ # @param [String, nil] code OAuth2 error code
65
+ # @param [String, nil] description OAuth2 error description
66
+ # @return [Hash] Options hash containing :error_description when present
49
67
  def parse_error_description(code, description)
50
68
  return {} unless code || description
51
69
 
@@ -1,19 +1,40 @@
1
1
  module OAuth2
2
+ # Mixin that redacts sensitive instance variables in #inspect output.
3
+ #
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.
2
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]
3
13
  def self.included(base)
4
14
  base.extend(ClassMethods)
5
15
  end
6
16
 
17
+ # Class-level helpers for configuring filtered attributes.
7
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]
8
23
  def filtered_attributes(*attributes)
9
24
  @filtered_attribute_names = attributes.map(&:to_sym)
10
25
  end
11
26
 
27
+ # The configured attribute names to filter.
28
+ #
29
+ # @return [Array<Symbol>]
12
30
  def filtered_attribute_names
13
31
  @filtered_attribute_names || []
14
32
  end
15
33
  end
16
34
 
35
+ # Custom inspect that redacts configured attributes.
36
+ #
37
+ # @return [String]
17
38
  def inspect
18
39
  filtered_attribute_names = self.class.filtered_attribute_names
19
40
  return super if filtered_attribute_names.empty?