diff --git a/class-two-factor-core.php b/class-two-factor-core.php index d98cbfe6..1743f611 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -687,26 +687,32 @@ public static function get_available_providers_for_user( $user = null ) { $user_providers_raw = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true ); /** - * If the user had enabled providers, but none of them exist currently, - * if emailed codes is available force it to be on, so that deprecated - * or removed providers don't result in the two-factor requirement being + * If the user had enabled providers in user meta, but none of those + * providers are still registered, force emailed codes on when available + * so deprecated or removed providers don't result in two-factor being * removed and 'failing open'. * - * Possible enhancement: add a filter to change the fallback method? + * If the configured providers are still registered, an empty enabled list + * may have been returned intentionally by two_factor_enabled_providers_for_user + * and must be respected. */ 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 ), - ) - ); + $still_registered = array_intersect( (array) $user_providers_raw, array_keys( $providers ) ); + + if ( empty( $still_registered ) ) { + 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/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index c265f6aa..f7fa908a 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -2267,6 +2267,49 @@ public function test_get_available_providers_for_user_with_configured_providers( $this->assertCount( 2, $available, 'Two providers are available' ); } + /** + * Ensure an intentionally emptied provider list is respected. + * + * @covers Two_Factor_Core::get_available_providers_for_user + */ + public function test_get_available_providers_for_user_respects_filter_cleared_list() { + $user = self::factory()->user->create_and_get(); + + update_user_meta( $user->ID, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Email' ) ); + + $filter = function ( $enabled_providers, $user_id ) use ( $user ) { + $this->assertSame( $user->ID, $user_id, 'Filter received expected user ID' ); + return array(); + }; + + add_filter( 'two_factor_enabled_providers_for_user', $filter, 10, 2 ); + + try { + $this->assertEmpty( + Two_Factor_Core::get_available_providers_for_user( $user->ID ), + 'No fallback provider is forced when the filter intentionally returns an empty list' + ); + } finally { + remove_filter( 'two_factor_enabled_providers_for_user', $filter, 10 ); + } + } + + /** + * Ensure fallback still applies when configured providers are no longer registered. + * + * @covers Two_Factor_Core::get_available_providers_for_user + */ + public function test_get_available_providers_for_user_falls_back_when_configured_providers_are_missing() { + $user = self::factory()->user->create_and_get(); + + update_user_meta( $user->ID, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Missing' ) ); + + $available = Two_Factor_Core::get_available_providers_for_user( $user->ID ); + + $this->assertCount( 1, $available, 'Email fallback remains active when configured providers are missing' ); + $this->assertArrayHasKey( 'Two_Factor_Email', $available, 'Emailed codes are forced on for missing configured providers' ); + } + /** * Verify process_provider() returns WP_Error when no provider is given. *