Skip to content

Commit 48c33a9

Browse files
committed
Introduce two_factor_revalidate_session action hook
This hook allows third-party plugins to easily trigger a 2FA re-authentication flow for sensitive actions. Fixes #644.
1 parent 6b44519 commit 48c33a9

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

class-two-factor-core.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ public static function add_hooks( $compat ) {
150150
add_action( 'admin_init', array( __CLASS__, 'trigger_user_settings_action' ) );
151151
add_filter( 'two_factor_providers', array( __CLASS__, 'enable_dummy_method_for_debug' ) );
152152

153+
add_action( 'two_factor_revalidate_session', array( __CLASS__, 'action_revalidate_session' ), 10, 2 );
154+
153155
// Add Settings link to plugin action links.
154156
add_filter( 'plugin_action_links_' . plugin_basename( TWO_FACTOR_DIR . 'two-factor.php' ), array( __CLASS__, 'add_settings_action_link' ) );
155157

@@ -607,6 +609,40 @@ public static function trigger_user_settings_action() {
607609
}
608610
}
609611

612+
/**
613+
* Require a recent Two Factor session.
614+
*
615+
* Triggers a redirect to the two-factor revalidation screen if the current session
616+
* hasn't been validated within the specified time window.
617+
*
618+
* @since NEXT
619+
*
620+
* @param int $time_window The grace period in seconds. Default 300 (5 minutes).
621+
* @param string $redirect_to The URL to redirect back to after revalidation. Defaults to the current request URI.
622+
*
623+
* @return void
624+
*/
625+
public static function action_revalidate_session( $time_window = 300, $redirect_to = '' ) {
626+
if ( ! is_user_logged_in() || ! self::is_user_using_two_factor() ) {
627+
return;
628+
}
629+
630+
$last_2fa = self::is_current_user_session_two_factor();
631+
$is_recent = $last_2fa && ( time() - $last_2fa < (int) $time_window );
632+
633+
if ( ! $is_recent ) {
634+
if ( empty( $redirect_to ) && isset( $_SERVER['REQUEST_URI'] ) ) {
635+
$redirect_to = wp_unslash( $_SERVER['REQUEST_URI'] );
636+
}
637+
638+
$reauth_url = self::get_user_two_factor_revalidate_url();
639+
$reauth_url = add_query_arg( 'redirect_to', urlencode( $redirect_to ), $reauth_url );
640+
641+
wp_safe_redirect( $reauth_url );
642+
exit;
643+
}
644+
}
645+
610646
/**
611647
* Keep track of all the authentication cookies that need to be
612648
* invalidated before the second factor authentication.

tests/class-two-factor-core.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2707,4 +2707,66 @@ public function test_add_settings_action_link() {
27072707
$this->assertStringContainsString( 'Settings', $first );
27082708
$this->assertStringContainsString( 'options-general.php', $first );
27092709
}
2710+
2711+
/**
2712+
* @covers Two_Factor_Core::action_revalidate_session
2713+
*/
2714+
public function test_action_revalidate_session_redirects_when_not_recent() {
2715+
$user_id = self::factory()->user->create();
2716+
wp_set_current_user( $user_id );
2717+
update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Dummy' ) );
2718+
update_user_meta( $user_id, Two_Factor_Core::PROVIDER_USER_META_KEY, 'Two_Factor_Dummy' );
2719+
2720+
// Simulate an old session (20 minutes ago)
2721+
$old_time = time() - ( 20 * MINUTE_IN_SECONDS );
2722+
Two_Factor_Core::update_current_user_session(
2723+
array(
2724+
'two-factor-provider' => 'Two_Factor_Dummy',
2725+
'two-factor-login' => $old_time,
2726+
)
2727+
);
2728+
2729+
$_SERVER['REQUEST_URI'] = '/wp-admin/post.php?post=1&action=edit';
2730+
2731+
// Catch the redirect exception
2732+
$redirected = false;
2733+
try {
2734+
Two_Factor_Core::action_revalidate_session( 300, '/custom-redirect/' );
2735+
} catch ( Two_Factor_Redirect_Exception $e ) {
2736+
$redirected = true;
2737+
$location = $e->getMessage();
2738+
$this->assertStringContainsString( 'action=revalidate_2fa', $location );
2739+
$this->assertStringContainsString( 'redirect_to=%2Fcustom-redirect%2F', $location );
2740+
}
2741+
2742+
$this->assertTrue( $redirected, 'Expected wp_safe_redirect to throw Two_Factor_Redirect_Exception.' );
2743+
}
2744+
2745+
/**
2746+
* @covers Two_Factor_Core::action_revalidate_session
2747+
*/
2748+
public function test_action_revalidate_session_bypasses_when_recent() {
2749+
$user_id = self::factory()->user->create();
2750+
wp_set_current_user( $user_id );
2751+
update_user_meta( $user_id, Two_Factor_Core::ENABLED_PROVIDERS_USER_META_KEY, array( 'Two_Factor_Dummy' ) );
2752+
2753+
// Simulate a recent session (1 minute ago)
2754+
$recent_time = time() - MINUTE_IN_SECONDS;
2755+
Two_Factor_Core::update_current_user_session(
2756+
array(
2757+
'two-factor-provider' => 'Two_Factor_Dummy',
2758+
'two-factor-login' => $recent_time,
2759+
)
2760+
);
2761+
2762+
// Should not throw an exception (no redirect)
2763+
$exception = false;
2764+
try {
2765+
Two_Factor_Core::action_revalidate_session( 300 );
2766+
} catch ( Two_Factor_Redirect_Exception $e ) {
2767+
$exception = true;
2768+
}
2769+
2770+
$this->assertFalse( $exception, 'Expected no redirect for a recent session.' );
2771+
}
27102772
}

0 commit comments

Comments
 (0)