Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ public static function add_hooks( $compat ) {
add_action( 'admin_init', array( __CLASS__, 'trigger_user_settings_action' ) );
add_filter( 'two_factor_providers', array( __CLASS__, 'enable_dummy_method_for_debug' ) );

add_action( 'two_factor_revalidate_session', array( __CLASS__, 'action_revalidate_session' ), 10, 2 );

// Add Settings link to plugin action links.
add_filter( 'plugin_action_links_' . plugin_basename( TWO_FACTOR_DIR . 'two-factor.php' ), array( __CLASS__, 'add_settings_action_link' ) );

Expand Down Expand Up @@ -603,6 +605,40 @@ public static function trigger_user_settings_action() {
}
}

/**
* Require a recent Two Factor session.
*
* Triggers a redirect to the two-factor revalidation screen if the current session
* hasn't been validated within the specified time window.
*
* @since NEXT
*
* @param int $time_window The grace period in seconds. Default 300 (5 minutes).
* @param string $redirect_to The URL to redirect back to after revalidation. Defaults to the current request URI.
*
* @return void
*/
public static function action_revalidate_session( $time_window = 300, $redirect_to = '' ) {
if ( ! is_user_logged_in() || ! self::is_user_using_two_factor() ) {
return;
}

$last_2fa = self::is_current_user_session_two_factor();
$is_recent = $last_2fa && ( time() - $last_2fa < (int) $time_window );

if ( ! $is_recent ) {
if ( empty( $redirect_to ) && isset( $_SERVER['REQUEST_URI'] ) ) {
$redirect_to = wp_unslash( $_SERVER['REQUEST_URI'] );
}

$reauth_url = self::get_user_two_factor_revalidate_url();
$reauth_url = add_query_arg( 'redirect_to', urlencode( $redirect_to ), $reauth_url );

wp_safe_redirect( $reauth_url );
exit;
}
}

/**
* Keep track of all the authentication cookies that need to be
* invalidated before the second factor authentication.
Expand Down
62 changes: 62 additions & 0 deletions tests/class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -2707,4 +2707,66 @@ public function test_add_settings_action_link() {
$this->assertStringContainsString( 'Settings', $first );
$this->assertStringContainsString( 'options-general.php', $first );
}

/**
* @covers Two_Factor_Core::action_revalidate_session
*/
public function test_action_revalidate_session_redirects_when_not_recent() {
$user_id = self::factory()->user->create();
wp_set_current_user( $user_id );
update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Dummy' ) );
update_user_meta( $user_id, Two_Factor_Core::PROVIDER_USER_META_KEY, 'Two_Factor_Dummy' );

// Simulate an old session (20 minutes ago)
$old_time = time() - ( 20 * MINUTE_IN_SECONDS );
Two_Factor_Core::update_current_user_session(
array(
'two-factor-provider' => 'Two_Factor_Dummy',
'two-factor-login' => $old_time,
)
);

$_SERVER['REQUEST_URI'] = '/wp-admin/post.php?post=1&action=edit';

// Catch the redirect exception
$redirected = false;
try {
Two_Factor_Core::action_revalidate_session( 300, '/custom-redirect/' );
} catch ( Two_Factor_Redirect_Exception $e ) {
$redirected = true;
$location = $e->getMessage();
$this->assertStringContainsString( 'action=revalidate_2fa', $location );
$this->assertStringContainsString( 'redirect_to=%2Fcustom-redirect%2F', $location );
}

$this->assertTrue( $redirected, 'Expected wp_safe_redirect to throw Two_Factor_Redirect_Exception.' );
}

/**
* @covers Two_Factor_Core::action_revalidate_session
*/
public function test_action_revalidate_session_bypasses_when_recent() {
$user_id = self::factory()->user->create();
wp_set_current_user( $user_id );
update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Dummy' ) );

// Simulate a recent session (1 minute ago)
$recent_time = time() - MINUTE_IN_SECONDS;
Two_Factor_Core::update_current_user_session(
array(
'two-factor-provider' => 'Two_Factor_Dummy',
'two-factor-login' => $recent_time,
)
);

// Should not throw an exception (no redirect)
$exception = false;
try {
Two_Factor_Core::action_revalidate_session( 300 );
} catch ( Two_Factor_Redirect_Exception $e ) {
$exception = true;
}

$this->assertFalse( $exception, 'Expected no redirect for a recent session.' );
}
}
Loading