Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trigger two-factor flow only when expected #660

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Changes from 5 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
32 changes: 22 additions & 10 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ class Two_Factor_Core {
*/
public static function add_hooks( $compat ) {
add_action( 'init', array( __CLASS__, 'get_providers' ) ); // @phpstan-ignore return.void
add_action( 'wp_login', array( __CLASS__, 'wp_login' ), 10, 2 );
add_filter( 'wp_login_errors', array( __CLASS__, 'maybe_show_reset_password_notice' ) );
add_action( 'after_password_reset', array( __CLASS__, 'clear_password_reset_notice' ) );
add_action( 'login_form_validate_2fa', array( __CLASS__, 'login_form_validate_2fa' ) );
Expand All @@ -117,8 +116,13 @@ public static function add_hooks( $compat ) {
add_action( 'set_auth_cookie', array( __CLASS__, 'collect_auth_cookie_tokens' ) );
add_action( 'set_logged_in_cookie', array( __CLASS__, 'collect_auth_cookie_tokens' ) );

// Run only after the core wp_authenticate_username_password() check.
add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate' ), 50 );
/**
* Enable the two-factor workflow only for login requests with username
* and without an existing user cookie.
*
* Run after core username/password and cookie checks.
*/
add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate' ), 31, 3 );
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Priority 31 is still after wp_authenticate_username_password() so the resolved $user will be set correctly.


// Run as late as possible to prevent other plugins from unintentionally bypassing.
add_filter( 'authenticate', array( __CLASS__, 'filter_authenticate_block_cookies' ), PHP_INT_MAX );
Expand Down Expand Up @@ -676,18 +680,26 @@ public static function destroy_current_session_for_user( $user ) {
* Prevent login through XML-RPC and REST API for users with at least one
* two-factor method enabled.
*
* @param WP_User|WP_Error $user Valid WP_User only if the previous filters
* @param WP_User|WP_Error $user Valid WP_User only if the previous filters
* have verified and confirmed the
* authentication credentials.
* @param string $username The username.
* @param string $password The password.
*
* @return WP_User|WP_Error
*/
public static function filter_authenticate( $user ) {
if ( $user instanceof WP_User && self::is_api_request() && self::is_user_using_two_factor( $user->ID ) && ! self::is_user_api_login_enabled( $user->ID ) ) {
return new WP_Error(
'invalid_application_credentials',
__( 'Error: API login for user disabled.', 'two-factor' )
);
public static function filter_authenticate( $user, $username, $password ) {
if ( ! empty( $username ) && $user instanceof WP_User && self::is_user_using_two_factor( $user->ID ) ) {
// Disable authentication requests for API requests for users with two-factor enabled.
if ( self::is_api_request() && ! self::is_user_api_login_enabled( $user->ID ) ) {
return new WP_Error(
'invalid_application_credentials',
__( 'Error: API login for user disabled.', 'two-factor' )
);
}

// Trigger the second-factor flow for valid users.
add_action( 'wp_login', array( __CLASS__, 'wp_login' ), PHP_INT_MAX, 2 );
Copy link
Collaborator Author

@kasparsd kasparsd Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another major difference here is that we're now triggering the wp_login much later to avoid any other integration plugins to do their job before we completely hijack the remaining login flow.

The only risk here is that some other plugin hijacks the login flow before us and issues a redirect, for example.

I add this sample hook:

add_action(
	'wp_login',
	function ( $user ) {
		if ( $user ) {
			wp_redirect( admin_url( '?redirect=test' ) );
			exit;
		}
	}
);

which returns a redirect before our wp_login, and the login session correctly fails because we're blocking the login cookies through filter_authenticate() attached to the authenticate hook which fires before wp_login.

no-cookies-on-redirect

}

return $user;
Expand Down
Loading