diff --git a/class-two-factor-core.php b/class-two-factor-core.php index d98cbfe6..84771e44 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -695,18 +695,44 @@ public static function get_available_providers_for_user( $user = null ) { * Possible enhancement: add a filter to change the fallback method? */ if ( empty( $enabled_providers ) && $user_providers_raw ) { - if ( isset( $providers['Two_Factor_Email'] ) ) { - // Force Emailed codes to 'on'. - $enabled_providers[] = 'Two_Factor_Email'; - } else { - return new WP_Error( - 'no_available_2fa_methods', - __( 'Error: You have Two Factor method(s) enabled, but the provider(s) no longer exist. Please contact a site administrator for assistance.', 'two-factor' ), - array( - 'user_providers_raw' => $user_providers_raw, - 'available_providers' => array_keys( $providers ), - ) - ); + // Determine whether the filter intentionally cleared the list, or + // whether the providers are genuinely missing/removed. Only apply + // the fallback filter in the former case — if providers no longer + // exist, the fail-safe always applies to prevent failing open. + $unfiltered = array_intersect( (array) $user_providers_raw, array_keys( $providers ) ); + + /** + * Filters whether the email provider fallback is applied when a user's + * enabled provider list resolves to empty but they have providers configured + * in user meta. Return false to disable the fallback and allow an empty + * provider list to pass through — for example, to bypass two-factor for + * trusted IP addresses. + * + * This filter only runs when the configured providers still exist. If + * providers are genuinely missing or removed, the fail-safe always applies + * regardless of this filter. + * + * @since 0.17.0 + * + * @param bool $apply_fallback Whether to apply the email fallback. Default true. + * @param int $user_id The user ID. + */ + $apply_fallback = empty( $unfiltered ) || apply_filters( 'two_factor_email_fallback_enabled', true, $user->ID ); + + if ( $apply_fallback ) { + if ( isset( $providers['Two_Factor_Email'] ) ) { + // Force Emailed codes to 'on'. + $enabled_providers[] = 'Two_Factor_Email'; + } else { + return new WP_Error( + 'no_available_2fa_methods', + __( 'Error: You have Two Factor method(s) enabled, but the provider(s) no longer exist. Please contact a site administrator for assistance.', 'two-factor' ), + array( + 'user_providers_raw' => $user_providers_raw, + 'available_providers' => array_keys( $providers ), + ) + ); + } } } diff --git a/readme.txt b/readme.txt index 1a23231c..c745b5d3 100644 --- a/readme.txt +++ b/readme.txt @@ -92,6 +92,7 @@ Here is a list of action and filter hooks provided by the plugin: - `two_factor_providers` filter overrides the available two-factor providers such as email and time-based one-time passwords. Array values are PHP classnames of the two-factor providers. - `two_factor_providers_for_user` filter overrides the available two-factor providers for a specific user. Array values are instances of provider classes and the user object `WP_User` is available as the second argument. - `two_factor_enabled_providers_for_user` filter overrides the list of two-factor providers enabled for a user. First argument is an array of enabled provider classnames as values, the second argument is the user ID. +- `two_factor_email_fallback_enabled` filter controls whether the email provider fallback is applied when a user's enabled provider list is empty but providers are configured in user meta. Return `false` to disable the fallback (e.g. to bypass two-factor for trusted IP addresses). The user ID is available as the second argument. - `two_factor_user_authenticated` action which receives the logged in `WP_User` object as the first argument for determining the logged in user right after the authentication workflow. - `two_factor_user_api_login_enable` filter restricts authentication for REST API and XML-RPC to application passwords only. Provides the user ID as the second argument. - `two_factor_email_token_ttl` filter overrides the time interval in seconds that an email token is considered after generation. Accepts the time in seconds as the first argument and the ID of the `WP_User` object being authenticated.