From 2d4b9ecfb2890d3f41c888778e0a74772a75c13a Mon Sep 17 00:00:00 2001 From: Dion Hulse Date: Mon, 4 Sep 2023 13:34:40 -0700 Subject: [PATCH] Destroy existing sessions when activating 2FA. --- class-two-factor-core.php | 17 +++- tests/class-two-factor-core.php | 143 ++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 6a990c87..f1b3045d 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -1543,9 +1543,9 @@ public static function user_two_factor_options_update( $user_id ) { return; } - $providers = self::get_providers(); - - $enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ]; + $providers = self::get_providers(); + $enabled_providers = $_POST[ self::ENABLED_PROVIDERS_USER_META_KEY ]; + $existing_providers = self::get_enabled_providers_for_user( $user_id ); // Enable only the available providers. $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) ); @@ -1556,6 +1556,17 @@ public static function user_two_factor_options_update( $user_id ) { if ( ! empty( $new_provider ) && in_array( $new_provider, $enabled_providers, true ) ) { update_user_meta( $user_id, self::PROVIDER_USER_META_KEY, $new_provider ); } + + // Destroy other sessions if we've activated a new provider. + if ( array_diff( $enabled_providers, $existing_providers ) ) { + if ( $user_id === get_current_user_id() ) { + // Keep the current session, destroy others sessions for this user. + wp_destroy_other_sessions(); + } else { + // Destroy all sessions for the user. + WP_Session_Tokens::get_instance( $user_id )->destroy_all(); + } + } } } diff --git a/tests/class-two-factor-core.php b/tests/class-two-factor-core.php index 0f3a1595..f6025f61 100644 --- a/tests/class-two-factor-core.php +++ b/tests/class-two-factor-core.php @@ -800,6 +800,149 @@ public function test_dont_notify_admin_when_filter_disabled() { reset_phpmailer_instance(); } + /** + * Validate that other sessions are destroyed once Two-Factor is enabled. + * + * @covers Two_Factor_Core::user_two_factor_options_update + */ + public function test_other_sessions_destroyed_when_enabling_2fa() { + $user_id = self::factory()->user->create( + array( + 'user_login' => 'username', + 'user_pass' => 'password', + ) + ); + + $user = new WP_User( $user_id ); + + $session_manager = WP_Session_Tokens::get_instance( $user->ID ); + + $this->assertCount( 0, $session_manager->get_all(), 'No user sessions are present first' ); + + // Generate multiple existing sessions. + $session_manager->create( time() + HOUR_IN_SECONDS ); + $session_manager->create( time() + DAY_IN_SECONDS ); + $this->assertCount( 2, $session_manager->get_all(), 'Can fetch active sessions' ); + + $user_authenticated = wp_signon( + array( + 'user_login' => 'username', + 'user_password' => 'password', + ) + ); + $this->assertEquals( $user_authenticated, $user, 'User can authenticate' ); + + // Enable Two-Factor for the user. + wp_set_current_user( $user->ID ); + + $key = '_nonce_user_two_factor_options'; + $nonce = wp_create_nonce( 'user_two_factor_options' ); + $_POST[ $key ] = $nonce; + $_REQUEST[ $key ] = $nonce; + + $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] = array( + 'Two_Factor_Dummy' => 'Two_Factor_Dummy' + ); + + Two_Factor_Core::user_two_factor_options_update( $user->ID ); + + // Validate that Two-Factor is now enabled. + $this->assertCount( 1, Two_Factor_Core::get_available_providers_for_user( $user->ID ) ); + $this->assertCount( 1, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ) ); + + // Validate that only the current session still exists. + $this->assertCount( 1, $session_manager->get_all(), 'All known authentication sessions have been destroyed' ); + + // Create another session, activate another provider, verify sessions are still valid. + $session_manager->create( time() + DAY_IN_SECONDS ); + $this->assertCount( 2, $session_manager->get_all(), 'Failed to create another session' ); + + $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] = array( + 'Two_Factor_Dummy' => 'Two_Factor_Dummy', + 'Two_Factor_Email' => 'Two_Factor_Email', + ); + + Two_Factor_Core::user_two_factor_options_update( $user->ID ); + + // Validate that Two-Factor is now enabled with two providers. + $this->assertCount( 2, Two_Factor_Core::get_available_providers_for_user( $user->ID ) ); + $this->assertCount( 2, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ) ); + + $this->assertCount( 1, $session_manager->get_all(), 'All known authentication sessions have been destroyed' ); + + // Create another session, disable a provider, verify both sessions still exist. + $session_manager->create( time() + DAY_IN_SECONDS ); + $this->assertCount( 2, $session_manager->get_all(), 'Failed to create another session' ); + + $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] = array( + 'Two_Factor_Dummy' => 'Two_Factor_Dummy', + ); + + Two_Factor_Core::user_two_factor_options_update( $user->ID ); + + // Validate that Two-Factor is now enabled with two providers. + $this->assertCount( 1, Two_Factor_Core::get_available_providers_for_user( $user->ID ) ); + $this->assertCount( 1, Two_Factor_Core::get_enabled_providers_for_user( $user->ID ) ); + + $this->assertCount( 2, $session_manager->get_all(), 'All known authentication sessions have been destroyed' ); + + } + + /** + * Validate the administrators sessions are not modified when modifying another user. + * + * @covers Two_Factor_Core::user_two_factor_options_update + */ + public function test_all_sessions_destroyed_when_enabling_2fa_by_admin() { + $admin_id = self::factory()->user->create( + array( + 'role' => 'administrator' + ) + ); + wp_set_current_user( $admin_id ); + + // Create an admin session,. + $admin_session_manager = WP_Session_Tokens::get_instance( $admin_id ); + + $admin_session_manager->create( time() + DAY_IN_SECONDS ); + $this->assertCount( 1, $admin_session_manager->get_all(), 'No admin sessions are present first' ); + + // Create the target user. + $user_id = self::factory()->user->create( + array( + 'user_login' => 'username', + 'user_pass' => 'password', + ) + ); + + $session_manager = WP_Session_Tokens::get_instance( $user_id ); + + $this->assertCount( 0, $session_manager->get_all(), 'No user sessions are present first' ); + + // Generate multiple existing sessions. + $session_manager->create( time() + DAY_IN_SECONDS ); + $this->assertCount( 1, $session_manager->get_all(), 'Can fetch active sessions' ); + + $key = '_nonce_user_two_factor_options'; + $nonce = wp_create_nonce( 'user_two_factor_options' ); + $_POST[ $key ] = $nonce; + $_REQUEST[ $key ] = $nonce; + + $_POST[ Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY ] = array( 'Two_Factor_Dummy' => 'Two_Factor_Dummy' ); + + Two_Factor_Core::user_two_factor_options_update( $user_id ); + + // Validate that Two-Factor is now enabled. + $this->assertCount( 1, Two_Factor_Core::get_available_providers_for_user( $user_id ) ); + $this->assertCount( 1, Two_Factor_Core::get_enabled_providers_for_user( $user_id ) ); + + // Validate the User has no sessions. + $this->assertCount( 0, $session_manager->get_all(), 'All known authentication sessions have been destroyed' ); + + // Validate that the Admin still has a session. + $this->assertCount( 1, $admin_session_manager->get_all(), 'No admin sessions are present first' ); + } + /** * @covers Two_Factor_Core::show_password_reset_error */