get_code( $this->get_token_length() ); update_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, time() ); update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) ); return $token; } /** * Check if user has a valid token already. * * @since 0.2.0 * * @param int $user_id User ID. * @return boolean If user has a valid email token. */ public function user_has_token( $user_id ) { $hashed_token = $this->get_user_token( $user_id ); if ( ! empty( $hashed_token ) ) { return true; } return false; } /** * Has the user token validity timestamp expired. * * @since 0.6.0 * * @param integer $user_id User ID. * * @return boolean */ public function user_token_has_expired( $user_id ) { $token_lifetime = $this->user_token_lifetime( $user_id ); $token_ttl = $this->user_token_ttl( $user_id ); // Invalid token lifetime is considered an expired token. if ( is_int( $token_lifetime ) && $token_lifetime <= $token_ttl ) { return false; } return true; } /** * Get the lifetime of a user token in seconds. * * @since 0.6.0 * * @param integer $user_id User ID. * * @return integer|null Return `null` if the lifetime can't be measured. */ public function user_token_lifetime( $user_id ) { $timestamp = intval( get_user_meta( $user_id, self::TOKEN_META_KEY_TIMESTAMP, true ) ); if ( ! empty( $timestamp ) ) { return time() - $timestamp; } return null; } /** * Return the token time-to-live for a user. * * @since 0.6.0 * * @param integer $user_id User ID. * * @return integer */ public function user_token_ttl( $user_id ) { $token_ttl = 15 * MINUTE_IN_SECONDS; /** * Filters the number of seconds the email token is considered valid after generation. * * @since 0.6.0 * @deprecated 0.11.0 Use {@see 'two_factor_email_token_ttl'} instead. * * @param int $token_ttl Token time-to-live in seconds. * @param int $user_id User ID. */ $token_ttl = (int) apply_filters_deprecated( 'two_factor_token_ttl', array( $token_ttl, $user_id ), '0.11.0', 'two_factor_email_token_ttl' ); /** * Filters the number of seconds the email token is considered valid after generation. * * @since 0.11.0 * * @param int $token_ttl Token time-to-live in seconds. * @param int $user_id User ID. */ return (int) apply_filters( 'two_factor_email_token_ttl', $token_ttl, $user_id ); } /** * Get the authentication token for the user. * * @since 0.2.0 * * @param int $user_id User ID. * * @return string|boolean User token or `false` if no token found. */ public function get_user_token( $user_id ) { $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true ); if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) { return $hashed_token; } return false; } /** * Validate the user token. * * @since 0.1-dev * * @param int $user_id User ID. * @param string $token User token. * @return boolean */ public function validate_token( $user_id, $token ) { $hashed_token = $this->get_user_token( $user_id ); // Bail if token is empty or it doesn't match. if ( empty( $hashed_token ) || ! hash_equals( wp_hash( $token ), $hashed_token ) ) { return false; } if ( $this->user_token_has_expired( $user_id ) ) { return false; } // Ensure the token can be used only once. $this->delete_token( $user_id ); return true; } /** * Delete the user token. * * @since 0.1-dev * * @param int $user_id User ID. */ public function delete_token( $user_id ) { delete_user_meta( $user_id, self::TOKEN_META_KEY ); } /** * Get the client IP address for the current request. * * @since 0.15.0 * * Note that the IP address is used only for information purposes * and is expected to be configured correctly, if behind proxy. * * @return string|null */ private function get_client_ip() { if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders -- don't have more reliable option for now. return preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPressVIPMinimum.Variables.ServerVariables.UserControlledHeaders, WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___SERVER__REMOTE_ADDR__ -- we're limit the allowed characters. } return null; } /** * Generate and email the user token. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. * @return bool Whether the email contents were sent successfully. */ public function generate_and_email_token( $user ) { $token = $this->generate_token( $user->ID ); $remote_ip = $this->get_client_ip(); $ttl_minutes = (int) ceil( $this->user_token_ttl( $user->ID ) / MINUTE_IN_SECONDS ); $subject = wp_strip_all_tags( sprintf( /* translators: %s: site name */ __( '[%s] Login confirmation code', 'two-factor' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) ); $message_parts = array( __( 'Please complete the login by entering the verification code below:', 'two-factor' ), $token, sprintf( /* translators: %d: number of minutes */ __( 'This code will expire in %d minutes.', 'two-factor' ), $ttl_minutes ), sprintf( /* translators: %1$s: IP address of user, %2$s: user login */ __( 'A user from IP address %1$s has successfully authenticated as %2$s. If this wasn\'t you, please change your password.', 'two-factor' ), $remote_ip, $user->user_login ), ); $message = wp_strip_all_tags( implode( "\n\n", $message_parts ) ); /** * Filters the token email subject. * * @since 0.5.2 * * @param string $subject The email subject line. * @param int $user_id The ID of the user. */ $subject = apply_filters( 'two_factor_token_email_subject', $subject, $user->ID ); /** * Filters the token email message. * * @since 0.5.2 * * @param string $message The email message. * @param string $token The token. * @param int $user_id The ID of the user. */ $message = apply_filters( 'two_factor_token_email_message', $message, $token, $user->ID ); return wp_mail( $user->user_email, $subject, $message ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail } /** * Prints the form that prompts the user to authenticate. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. */ public function authentication_page( $user ) { if ( ! $user ) { return; } if ( ! $this->user_has_token( $user->ID ) || $this->user_token_has_expired( $user->ID ) ) { $this->generate_and_email_token( $user ); } $token_length = $this->get_token_length(); $token_placeholder = str_repeat( 'X', $token_length ); require_once ABSPATH . '/wp-admin/includes/template.php'; ?>
ID ) && isset( $_REQUEST[ self::INPUT_NAME_RESEND_CODE ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- non-distructive option that relies on user state. $this->generate_and_email_token( $user ); return true; } return false; } /** * Validates the users input token. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ public function validate_authentication( $user ) { $code = $this->sanitize_code_from_request( 'two-factor-email-code' ); if ( ! isset( $user->ID ) || ! $code ) { return false; } return $this->validate_token( $user->ID, $code ); } /** * Whether this Two Factor provider is configured and available for the user specified. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. * @return boolean */ public function is_available_for_user( $user ) { return true; } /** * Inserts markup at the end of the user profile field for this provider. * * @since 0.1-dev * * @param WP_User $user WP_User object of the logged-in user. */ public function user_options( $user ) { $email = $user->user_email; ?>