_Opaque_Tokens::delete_token( $token );
$user_id = $data['user_id'];
try {
$lockout = $data['lockout_id'] ? $itsec_lockout->get_lockout( $data['lockout_id'], OBJECT ) : false;
} catch ( \Exception $e ) {
wp_die();
}
$success = ! $user_id || $this->send_lockout_bypass_email( $user_id, $data['lockout_id'] );
$show_login = $lockout && ! $lockout->get_host();
if ( $success ) {
$this->info_message = esc_html__( 'Please check your email for an authorized login link.', 'it-l10n-ithemes-security-pro' );
if ( ! $show_login ) {
wp_die( $this->info_message, '', array( 'response' => 200 ) );
}
} else {
$this->error->add( self::E_MAIL_FAILED, esc_html__( 'The email could not be sent.', 'it-l10n-ithemes-security-pro' ) );
if ( ! $show_login ) {
wp_die( $this->error );
}
}
}
/**
* Maybe prevent a lockout from occurring.
*
* @param bool $do_lockout
* @param Lockout\Context $context
*
* @return bool True to do the lockout, false not to.
*/
public function maybe_prevent_do_lockout( $do_lockout, $context ) {
$token_data = $this->verify_and_get_token_data();
if ( is_wp_error( $token_data ) ) {
return $do_lockout;
}
if ( $context instanceof Lockout\User_Context && $context->get_user_id() === $token_data['user'] ) {
return false;
}
if (
$context instanceof Lockout\Host_Context &&
$context->get_host() === $token_data['host'] &&
$context->get_login_user_id() === $token_data['user']
) {
return false;
}
return $do_lockout;
}
/**
* Prevent the user lockout from firing if the user has valid tokens.
*
* @param bool $execute
* @param Execute_Lock\Context $context
*
* @return bool True to execute the lock, false not to.
*/
public function maybe_prevent_execute_lock( $execute, $context ) {
$is_login = isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'];
if ( $is_login && isset( $_REQUEST['action'] ) && self::ACTION === $_REQUEST['action'] ) {
return false;
}
$token_data = $this->verify_and_get_token_data();
if ( is_wp_error( $token_data ) ) {
if ( $token_data->get_error_code() !== self::E_MISSING ) {
ITSEC_Lib::add_to_wp_error( $this->error, $token_data );
}
return $execute;
}
if ( $user = $this->get_user_from_context( $context ) ) {
return $token_data['user'] === $user->ID ? false : $execute;
}
return $execute;
}
/**
* When a user logs in successfully, clear their magic link token and any host lockouts.
*
* @param string $username
* @param WP_User $user
*/
public function cleanup_on_login( $username, $user ) {
if ( ! $token = $this->extract_token_from_state() ) {
return;
}
ITSEC_Lib::clear_cookie( self::COOKIE_VAR );
ITSEC_Lib::load( 'opaque-tokens' );
$data = ITSEC_Lib_Opaque_Tokens::verify_and_get_token_data( self::OT_LOCKOUT_BYPASS, $token, self::EXPIRES );
if ( is_wp_error( $data ) ) {
return;
}
if ( $data['user'] !== $user->ID ) {
return;
}
ITSEC_Lib_Opaque_Tokens::delete_token( $token );
}
/**
* Set a cookie from the magic link URL vars.
*/
public function set_cookie_for_magic_link() {
/** @var ITSEC_Lockout $itsec_lockout */
global $itsec_lockout;
if ( empty( $_REQUEST[ self::TOKEN_VAR ] ) ) {
return;
}
ITSEC_Lib::load( 'opaque-tokens' );
$data = ITSEC_Lib_Opaque_Tokens::verify_and_get_token_data( self::OT_LOCKOUT_BYPASS, $_REQUEST[ self::TOKEN_VAR ], self::EXPIRES );
if ( is_wp_error( $data ) ) {
ITSEC_Lib::add_to_wp_error( $this->error, $data );
return;
}
ITSEC_Lib::set_cookie( self::COOKIE_VAR, $_REQUEST[ self::TOKEN_VAR ], array(
'length' => self::EXPIRES,
) );
if ( ! empty( $data['lockout_id'] ) ) {
$itsec_lockout->release_lockout( $data['lockout_id'] );
}
}
/**
* Display an error if the email to send the login page link failed.
*
* @param WP_Error $errors
*
* @return WP_Error
*/
public function report_error_on_wp_login( $errors ) {
if ( ! is_wp_error( $errors ) ) {
$errors = new WP_Error();
}
ITSEC_Lib::add_to_wp_error( $this->error, $errors );
if ( $this->info_message ) {
$errors->add( 'sent', $this->info_message, 'message' );
}
return $errors;
}
/**
* Generate a link to the login page that will allow a user to login even if a brute force lockout exists.
*
* @param WP_User|int|string $user
* @param int $lockout_id
*
* @return string|false
*/
public function generate_login_page_link( $user, $lockout_id = 0 ) {
$user = ITSEC_Lib::get_user( $user );
if ( ! $user ) {
return false;
}
ITSEC_Lib::load( 'opaque-tokens' );
$token = ITSEC_Lib_Opaque_Tokens::create_token( self::OT_LOCKOUT_BYPASS, array(
'user' => (int) $user->ID,
'host' => ITSEC_Lib::get_ip(),
'lockout_id' => $lockout_id,
) );
if ( ! $token ) {
return false;
}
return add_query_arg( self::TOKEN_VAR, urlencode( $token ), ITSEC_Lib::get_login_url() );
}
/**
* Register the Magic Links notifications.
*
* @param array $notifications
*
* @return array
*/
public function register_notifications( $notifications ) {
$notifications[ self::NOTIFICATION ] = array(
'recipient' => ITSEC_Notification_Center::R_USER,
'schedule' => ITSEC_Notification_Center::S_NONE,
'subject_editable' => true,
'message_editable' => true,
'tags' => array( 'username', 'display_name', 'login_url', 'site_title', 'site_url' ),
'module' => 'magic-links',
);
return $notifications;
}
/**
* Register strings for the Magic Links Lockout Bypass notification.
*
* @return array
*/
public function notification_strings() {
return array(
'label' => __( 'Magic Links Lockout Bypass', 'it-l10n-ithemes-security-pro' ),
'description' => sprintf(
__( 'The %1$sMagic Links%2$s module sends an email with a Magic Link that bypasses a lockout. Note: the default email template already includes the %3$s tag.', 'it-l10n-ithemes-security-pro' ),
ITSEC_Core::get_link_for_settings_route( ITSEC_Core::get_settings_module_route( 'magic-links' ) ),
'',
'login_url
'
),
'tags' => array(
'username' => __( 'The recipient’s WordPress username.', 'it-l10n-ithemes-security-pro' ),
'display_name' => __( 'The recipient’s WordPress display name.', 'it-l10n-ithemes-security-pro' ),
'login_url' => __( 'The magic login link to continue logging in.', 'it-l10n-ithemes-security-pro' ),
'site_title' => __( 'The WordPress Site Title. Can be changed under Settings → General → Site Title', 'it-l10n-ithemes-security-pro' ),
'site_url' => __( 'The URL to your website.', 'it-l10n-ithemes-security-pro' ),
),
'subject' => __( 'Login Link', 'it-l10n-ithemes-security-pro' ),
'message' => __( 'Hi {{ $display_name }},
For security purposes, please click the button below to login.
Regards,
All at {{ $site_title }}', 'it-l10n-ithemes-security-pro' ),
);
}
/**
* Send the link to an unlocked login page to a given user.
*
* @param WP_User|int|string $user
* @param int $lockout_id
*
* @return bool
*/
private function send_lockout_bypass_email( $user, $lockout_id = 0 ) {
$user = ITSEC_Lib::get_user( $user );
$link = $this->generate_login_page_link( $user, $lockout_id );
if ( ! $link ) {
return false;
}
$nc = ITSEC_Core::get_notification_center();
$mail = $nc->mail();
$mail->set_recipients( array( $user->user_email ) );
$mail->add_user_header( esc_html__( 'Login Link', 'it-l10n-ithemes-security-pro' ), sprintf( esc_html__( 'Secure login link for %s', 'it-l10n-ithemes-security-pro' ), '' . get_bloginfo( 'name', 'display' ) . '' ), true );
$mail->add_text( ITSEC_Lib::replace_tags( $nc->get_message( self::NOTIFICATION ), array(
'username' => $user->user_login,
'display_name' => $user->display_name,
'login_url' => $link,
'site_title' => get_bloginfo( 'name', 'display' ),
'site_url' => $mail->get_display_url(),
) ) );
$mail->add_button( esc_html__( 'Continue Login', 'it-l10n-ithemes-security-pro' ), $link );
$mail->add_user_footer();
return $nc->send( self::NOTIFICATION, $mail );
}
/**
* Get the user from a lock context.
*
* @param Execute_Lock\Context $context
*
* @return WP_User|null
*/
private function get_user_from_context( Execute_Lock\Context $context ) {
if ( $context instanceof Execute_Lock\User_Context ) {
return get_userdata( $context->get_user_id() ) ?: null;
}
if ( $context instanceof Execute_Lock\Host_Context && $user_id = $context->get_login_user_id() ) {
return get_userdata( $user_id ) ?: null;
}
$source = $context->get_source();
if ( $source instanceof Lockout\Host_Context && $user_id = $source->get_login_user_id() ) {
return get_userdata( $user_id ) ?: null;
}
return null;
}
/**
* Get the user from a lock context.
*
* @param Execute_Lock\Context $context
*
* @return string
*/
private function get_username_from_context( Execute_Lock\Context $context ) {
if ( $context instanceof Execute_Lock\Username_Context ) {
return $context->get_username();
}
if ( $context instanceof Execute_Lock\Host_Context && $username = $context->get_login_username() ) {
return $username;
}
$source = $context->get_source();
if ( $source instanceof Lockout\Host_Context && $username = $source->get_login_username() ) {
return $username;
}
return null;
}
/**
* Check for valid magic link tokens.
*
* @return array|WP_Error
*/
private function verify_and_get_token_data() {
$token = $this->extract_token_from_state();
if ( ! $token ) {
return new WP_Error( self::E_MISSING, esc_html__( 'No magic link token found.', 'it-l10n-ithemes-security-pro' ) );
}
ITSEC_Lib::load( 'opaque-tokens' );
$data = ITSEC_Lib_Opaque_Tokens::verify_and_get_token_data( self::OT_LOCKOUT_BYPASS, $token, self::EXPIRES );
if ( isset( $_COOKIE[ self::COOKIE_VAR ] ) && is_wp_error( $data ) ) {
ITSEC_Lib::clear_cookie( self::COOKIE_VAR );
}
return $data;
}
/**
* Extract the token pair from the request state.
*
* @return string
*/
private function extract_token_from_state() {
if ( ! empty( $_REQUEST[ self::TOKEN_VAR ] ) ) {
return $_REQUEST[ self::TOKEN_VAR ];
}
if ( ! empty( $_COOKIE[ self::COOKIE_VAR ] ) ) {
return $_COOKIE[ self::COOKIE_VAR ];
}
return '';
}
}