init(); } /** * Loads the plugin's text domain. * * Sites on WordPress 4.6+ benefit from just-in-time loading of translations. */ public static function load_textdomain() { load_plugin_textdomain( 'two-factor' ); } /** * For each provider, include it and then instantiate it. * * @since 0.1-dev * * @return array */ public static function get_providers() { $providers = array( 'Two_Factor_Email' => TWO_FACTOR_DIR . 'providers/class-two-factor-email.php', 'Two_Factor_Totp' => TWO_FACTOR_DIR . 'providers/class-two-factor-totp.php', 'Two_Factor_FIDO_U2F' => TWO_FACTOR_DIR . 'providers/class-two-factor-fido-u2f.php', 'Two_Factor_Backup_Codes' => TWO_FACTOR_DIR . 'providers/class-two-factor-backup-codes.php', 'Two_Factor_Dummy' => TWO_FACTOR_DIR . 'providers/class-two-factor-dummy.php', ); /** * Filter the supplied providers. * * This lets third-parties either remove providers (such as Email), or * add their own providers (such as text message or Clef). * * @param array $providers A key-value array where the key is the class name, and * the value is the path to the file containing the class. */ $providers = apply_filters( 'two_factor_providers', $providers ); // FIDO U2F is PHP 5.3+ only. if ( isset( $providers['Two_Factor_FIDO_U2F'] ) && version_compare( PHP_VERSION, '5.3.0', '<' ) ) { unset( $providers['Two_Factor_FIDO_U2F'] ); trigger_error( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error sprintf( /* translators: %s: version number */ __( 'FIDO U2F is not available because you are using PHP %s. (Requires 5.3 or greater)', 'two-factor' ), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped PHP_VERSION ) ); } /** * For each filtered provider, */ foreach ( $providers as $class => $path ) { include_once $path; /** * Confirm that it's been successfully included before instantiating. */ if ( class_exists( $class ) ) { try { $providers[ $class ] = call_user_func( array( $class, 'get_instance' ) ); } catch ( Exception $e ) { unset( $providers[ $class ] ); } } } return $providers; } /** * Enable the dummy method only during debugging. * * @param array $methods List of enabled methods. * * @return array */ public static function enable_dummy_method_for_debug( $methods ) { if ( ! self::is_wp_debug() ) { unset( $methods['Two_Factor_Dummy'] ); } return $methods; } /** * Check if the debug mode is enabled. * * @return boolean */ protected static function is_wp_debug() { return ( defined( 'WP_DEBUG' ) && WP_DEBUG ); } /** * Get the user settings page URL. * * Fetch this from the plugin core after we introduce proper dependency injection * and get away from the singletons at the provider level (should be handled by core). * * @param integer $user_id User ID. * * @return string */ protected static function get_user_settings_page_url( $user_id ) { $page = 'user-edit.php'; if ( defined( 'IS_PROFILE_PAGE' ) && IS_PROFILE_PAGE ) { $page = 'profile.php'; } return add_query_arg( array( 'user_id' => intval( $user_id ), ), self_admin_url( $page ) ); } /** * Get the URL for resetting the secret token. * * @param integer $user_id User ID. * @param string $action Custom two factor action key. * * @return string */ public static function get_user_update_action_url( $user_id, $action ) { return wp_nonce_url( add_query_arg( array( self::USER_SETTINGS_ACTION_QUERY_VAR => $action, ), self::get_user_settings_page_url( $user_id ) ), sprintf( '%d-%s', $user_id, $action ), self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ); } /** * Check if a user action is valid. * * @param integer $user_id User ID. * @param string $action User action ID. * * @return boolean */ public static function is_valid_user_action( $user_id, $action ) { $request_nonce = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_NONCE_QUERY_ARG ] ) : ''; if ( ! $user_id || ! $action || ! $request_nonce ) { return false; } return wp_verify_nonce( $request_nonce, sprintf( '%d-%s', $user_id, $action ) ); } /** * Get the ID of the user being edited. * * @return integer */ public static function current_user_being_edited() { // Try to resolve the user ID from the request first. if ( ! empty( $_REQUEST['user_id'] ) ) { $user_id = intval( $_REQUEST['user_id'] ); if ( current_user_can( 'edit_user', $user_id ) ) { return $user_id; } } return get_current_user_id(); } /** * Trigger our custom update action if a valid * action request is detected and passes the nonce check. * * @return void */ public static function trigger_user_settings_action() { $action = isset( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) ? wp_unslash( $_REQUEST[ self::USER_SETTINGS_ACTION_QUERY_VAR ] ) : ''; $user_id = self::current_user_being_edited(); if ( self::is_valid_user_action( $user_id, $action ) ) { /** * This action is triggered when a valid Two Factor settings * action is detected and it passes the nonce validation. * * @param integer $user_id User ID. * @param string $action Settings action. */ do_action( 'two_factor_user_settings_action', $user_id, $action ); } } /** * Keep track of all the authentication cookies that need to be * invalidated before the second factor authentication. * * @param string $cookie Cookie string. * * @return void */ public static function collect_auth_cookie_tokens( $cookie ) { $parsed = wp_parse_auth_cookie( $cookie ); if ( ! empty( $parsed['token'] ) ) { self::$password_auth_tokens[] = $parsed['token']; } } /** * Fetch the WP_User object for a provided input. * * @since 0.8.0 * * @param int|WP_User $user Optional. The WP_User or user ID. Defaults to current user. * * @return false|WP_User WP_User on success, false on failure. */ public static function fetch_user( $user = null ) { if ( null === $user ) { $user = wp_get_current_user(); } elseif ( ! ( $user instanceof WP_User ) ) { $user = get_user_by( 'id', $user ); } if ( ! $user || ! $user->exists() ) { return false; } return $user; } /** * Get all Two-Factor Auth providers that are enabled for the specified|current user. * * @param int|WP_User $user Optonal. User ID, or WP_User object of the the user. Defaults to current user. * @return array */ public static function get_enabled_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); if ( ! $user ) { return array(); } $providers = self::get_providers(); $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_META_KEY, true ); if ( empty( $enabled_providers ) ) { $enabled_providers = array(); } $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) ); /** * Filter the enabled two-factor authentication providers for this user. * * @param array $enabled_providers The enabled providers. * @param int $user_id The user ID. */ return apply_filters( 'two_factor_enabled_providers_for_user', $enabled_providers, $user->ID ); } /** * Get all Two-Factor Auth providers that are both enabled and configured for the specified|current user. * * @param int|WP_User $user Optonal. User ID, or WP_User object of the the user. Defaults to current user. * @return array */ public static function get_available_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); if ( ! $user ) { return array(); } $providers = self::get_providers(); $enabled_providers = self::get_enabled_providers_for_user( $user ); $configured_providers = array(); foreach ( $providers as $classname => $provider ) { if ( in_array( $classname, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) { $configured_providers[ $classname ] = $provider; } } return $configured_providers; } /** * Gets the Two-Factor Auth provider for the specified|current user. * * @since 0.1-dev * * @param int|WP_User $user Optonal. User ID, or WP_User object of the the user. Defaults to current user. * @return object|null */ public static function get_primary_provider_for_user( $user = null ) { $user = self::fetch_user( $user ); if ( ! $user ) { return null; } $providers = self::get_providers(); $available_providers = self::get_available_providers_for_user( $user ); // If there's only one available provider, force that to be the primary. if ( empty( $available_providers ) ) { return null; } elseif ( 1 === count( $available_providers ) ) { $provider = key( $available_providers ); } else { $provider = get_user_meta( $user->ID, self::PROVIDER_USER_META_KEY, true ); // If the provider specified isn't enabled, just grab the first one that is. if ( ! isset( $available_providers[ $provider ] ) ) { $provider = key( $available_providers ); } } /** * Filter the two-factor authentication provider used for this user. * * @param string $provider The provider currently being used. * @param int $user_id The user ID. */ $provider = apply_filters( 'two_factor_primary_provider_for_user', $provider, $user->ID ); if ( isset( $providers[ $provider ] ) ) { return $providers[ $provider ]; } return null; } /** * Quick boolean check for whether a given user is using two-step. * * @since 0.1-dev * * @param int|WP_User $user Optonal. User ID, or WP_User object of the the user. Defaults to current user. * @return bool */ public static function is_user_using_two_factor( $user = null ) { $provider = self::get_primary_provider_for_user( $user ); return ! empty( $provider ); } /** * Handle the browser-based login. * * @since 0.1-dev * * @param string $user_login Username. * @param WP_User $user WP_User object of the logged-in user. */ public static function wp_login( $user_login, $user ) { if ( ! self::is_user_using_two_factor( $user->ID ) ) { return; } // Invalidate the current login session to prevent from being re-used. self::destroy_current_session_for_user( $user ); // Also clear the cookies which are no longer valid. wp_clear_auth_cookie(); self::show_two_factor_login( $user ); exit; } /** * Destroy the known password-based authentication sessions for the current user. * * Is there a better way of finding the current session token without * having access to the authentication cookies which are just being set * on the first password-based authentication request. * * @param \WP_User $user User object. * * @return void */ public static function destroy_current_session_for_user( $user ) { $session_manager = WP_Session_Tokens::get_instance( $user->ID ); foreach ( self::$password_auth_tokens as $auth_token ) { $session_manager->destroy( $auth_token ); } } /** * 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 * have verified and confirmed the * authentication credentials. * * @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' ) ); } return $user; } /** * Prevent login cookies being set on login for Two Factor users. * * This makes it so that Core never sends the auth cookies. `login_form_validate_2fa()` will send them manually once the 2nd factor has been verified. * * @param WP_User|WP_Error $user Valid WP_User only if the previous filters * have verified and confirmed the * authentication credentials. * * @return WP_User|WP_Error */ public static function filter_authenticate_block_cookies( $user ) { /* * NOTE: The `login_init` action is checked for here to ensure we're within the regular login flow, * rather than through an unsupported 3rd-party login process which this plugin doesn't support. */ if ( $user instanceof WP_User && self::is_user_using_two_factor( $user->ID ) && did_action( 'login_init' ) ) { add_filter( 'send_auth_cookies', '__return_false', PHP_INT_MAX ); } return $user; } /** * If the current user can login via API requests such as XML-RPC and REST. * * @param integer $user_id User ID. * * @return boolean */ public static function is_user_api_login_enabled( $user_id ) { return (bool) apply_filters( 'two_factor_user_api_login_enable', false, $user_id ); } /** * Is the current request an XML-RPC or REST request. * * @return boolean */ public static function is_api_request() { if ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) { return true; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return true; } return false; } /** * Display the login form. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. */ public static function show_two_factor_login( $user ) { if ( ! $user ) { $user = wp_get_current_user(); } $login_nonce = self::create_login_nonce( $user->ID ); if ( ! $login_nonce ) { wp_die( esc_html__( 'Failed to create a login nonce.', 'two-factor' ) ); } $redirect_to = isset( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : admin_url(); self::login_html( $user, $login_nonce['key'], $redirect_to ); } /** * Displays a message informing the user that their account has had failed login attempts. * * @param WP_User $user WP_User object of the logged-in user. */ public static function maybe_show_last_login_failure_notice( $user ) { $last_failed_two_factor_login = (int) get_user_meta( $user->ID, self::USER_RATE_LIMIT_KEY, true ); $failed_login_count = (int) get_user_meta( $user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY, true ); if ( $last_failed_two_factor_login ) { echo '
'; } } /** * Show the password reset notice if the user's password was reset. * * They were also sent an email notification in `send_password_reset_email()`, but email sent from a typical * web server is not reliable enough to trust completely. * * @param WP_Error $errors */ public static function maybe_show_reset_password_notice( $errors ) { if ( 'incorrect_password' !== $errors->get_error_code() ) { return $errors; } if ( ! isset( $_POST['log'] ) ) { return $errors; } $user_name = sanitize_user( wp_unslash( $_POST['log'] ) ); $attempted_user = get_user_by( 'login', $user_name ); if ( ! $attempted_user && str_contains( $user_name, '@' ) ) { $attempted_user = get_user_by( 'email', $user_name ); } if ( ! $attempted_user ) { return $errors; } $password_was_reset = get_user_meta( $attempted_user->ID, self::USER_PASSWORD_WAS_RESET_KEY, true ); if ( ! $password_was_reset ) { return $errors; } $errors->remove( 'incorrect_password' ); $errors->add( 'two_factor_password_reset', sprintf( __( 'Your password was reset because of too many failed Two Factor attempts. You will need to create a new password to regain access. Please check your email for more information.', 'two-factor' ), esc_url( add_query_arg( 'action', 'lostpassword', wp_login_url() ) ) ) ); return $errors; } /** * Clear the password reset notice after the user resets their password. * * @param WP_User $user */ public static function clear_password_reset_notice( $user ) { delete_user_meta( $user->ID, self::USER_PASSWORD_WAS_RESET_KEY ); } /** * Generates the html form for the second step of the authentication process. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. * @param string $login_nonce A string nonce stored in usermeta. * @param string $redirect_to The URL to which the user would like to be redirected. * @param string $error_msg Optional. Login error message. * @param string|object $provider An override to the provider. */ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg = '', $provider = null ) { if ( empty( $provider ) ) { $provider = self::get_primary_provider_for_user( $user->ID ); } elseif ( is_string( $provider ) && method_exists( $provider, 'get_instance' ) ) { $provider = call_user_func( array( $provider, 'get_instance' ) ); } $provider_class = get_class( $provider ); $available_providers = self::get_available_providers_for_user( $user ); $backup_providers = array_diff_key( $available_providers, array( $provider_class => null ) ); $interim_login = isset( $_REQUEST['interim-login'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $rememberme = intval( self::rememberme() ); if ( ! function_exists( 'login_header' ) ) { // We really should migrate login_header() out of `wp-login.php` so it can be called from an includes file. include_once TWO_FACTOR_DIR . 'includes/function.login-header.php'; } login_header(); if ( ! empty( $error_msg ) ) { echo '