it() { if ((!is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) && is_user_logged_in() && file_exists($this->includes_dir().'/tfa_frontend.php')) { $this->load_frontend(); } else { add_shortcode('twofactor_user_settings', array($this, 'shortcode_when_not_logged_in')); } } /** * Return the TOTP provider object. * * @param String $controller_id - which controller * * @return Simba_TFA_Provider_totp */ public function get_controller($controller_id = 'totp') { return $this->controllers[$controller_id]; } /** * Return all OTP controllers * * @return Array */ public function get_controllers() { return $this->controllers; } /** * Deprecated synonym for get_controller('totp') * * @return Simba_TFA_Provider_totp */ public function get_totp_controller() { trigger_error("Deprecated: Call get_controller('totp'), not get_totp_controller()", E_USER_WARNING); return $this->get_controller('totp'); } /** * "Shared" - i.e. could be called from either front-end or back-end */ public function shared_ajax() { if (empty($_POST['subaction']) || empty($_POST['nonce']) || !is_user_logged_in() || !wp_verify_nonce($_POST['nonce'], 'tfa_shared_nonce')) die('Security check (3).'); global $current_user; $subaction = $_POST['subaction']; if ('refreshotp' == $subaction) { $code = $this->get_controller('totp')->get_current_code($current_user->ID); if (false === $code) die(json_encode(array('code' => ''))); die(json_encode(array('code' => $code))); } elseif ('untrust_device' == $subaction && isset($_POST['device_id'])) { $this->untrust_device(stripslashes($_POST['device_id'])); ob_start(); $this->include_template('trusted-devices-inner-box.php', array('trusted_devices' => $this->user_get_trusted_devices())); echo json_encode(array('trusted_list' => ob_get_clean())); } exit; } /** * Mark a device as untrusted for the current user * * @param String $device_id */ protected function untrust_device($device_id) { $trusted_devices = $this->user_get_trusted_devices(); unset($trusted_devices[$device_id]); global $current_user; $current_user_id = $current_user->ID; $this->user_set_trusted_devices($current_user_id, $trusted_devices); } /** * Called upon the AJAX action simbatfa-init-otp . Will die. * * Uses these keys from $_POST: user */ public function tfaInitLogin() { if (empty($_POST['user'])) die('Security check (2).'); if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) { $res = array('result' => false, 'user_can_trust' => false); } else { if (!function_exists('sanitize_user')) require_once ABSPATH.WPINC.'/formatting.php'; // WP's password-checking sanitizes the supplied user, so we must do the same to check if TFA is enabled for them $auth_info = array('log' => sanitize_user(stripslashes((string)$_POST['user']))); if (!empty($_COOKIE['simbatfa_trust_token'])) $auth_info['trust_token'] = (string) $_COOKIE['simbatfa_trust_token']; $res = $this->pre_auth($auth_info, 'array'); } $results = array( 'jsonstarter' => 'justhere', 'status' => $res['result'], ); if (!empty($res['user_can_trust'])) { $results['user_can_trust'] = 1; if (!empty($res['user_already_trusted'])) $results['user_already_trusted'] = 1; } if (!empty($this->output_buffering)) { if (!empty($this->logged)) { $results['php_output'] = $this->logged; } restore_error_handler(); $buffered = ob_get_clean(); if ($buffered) $results['extra_output'] = $buffered; } $results = apply_filters('simbatfa_check_tfa_requirements_ajax_response', $results); echo json_encode($results); exit; } /** * Enable or disable TFA for a user * * @param Integer $user_id - the WordPress user ID * @param String $setting - either "true" (to turn on) or "false" (to turn off) */ public function change_tfa_enabled_status($user_id, $setting) { $previously_enabled = $this->is_activated_by_user($user_id) ? 1 : 0; $setting = ('true' === $setting) ? 1 : 0; update_user_meta($user_id, 'tfa_enable_tfa', $setting); do_action('simba_tfa_activation_status_saved', $user_id, $setting, $previously_enabled, $this); } /** * Here's where the login action happens. Called on the WP 'authenticate' action (which also happens when wp-login.php loads, so parameters need checking). * * @param WP_Error|WP_User $user * @param String $username - this is not necessarily the WP username; it is whatever was typed in the form, so can be an email address * @param String $password * * @return WP_Error|WP_User */ public function tfaVerifyCodeAndUser($user, $username, $password) { // Do not require a TFA code when authenticating via cookie (or other non-login-form mechanism) if ('' === $username && is_multisite()) return $user; // When both the AIOWPS and Two Factor Authentication plugins are active, this function is called more than once; that should be short-circuited. if (isset(self::$is_authenticated[$this->authentication_slug]) && self::$is_authenticated[$this->authentication_slug]) { return $user; } if (is_a($user, 'WP_User') && !empty($user->ID)) { if (in_array($user->ID, $this->application_passwords_authenticated)) { // User authenticated via an application password (and thus - see wp_authenticate_application_password - via an API request). Do not require a TFA code. return $user; } } $original_user = $user; $params = stripslashes_deep($_POST); // If (only) the error was a wrong password, but it looks like the user appended a TFA code to their password, then have another go if (is_wp_error($user) && array('incorrect_password') == $user->get_error_codes() && !isset($params['two_factor_code']) && false !== ($from_password = apply_filters('simba_tfa_tfa_from_password', false, $password))) { // This forces a new password authentication below $user = false; } if (is_wp_error($user)) { $ret = $user; } else { if (is_object($user) && isset($user->ID) && isset($user->user_login)) { $params['log'] = $user->user_login; // Confirm that this is definitely a username regardless of its format $may_be_email = false; } else { $params['log'] = $username; $may_be_email = true; } $params['caller'] = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['REQUEST_URI']; if (!empty($_COOKIE['simbatfa_trust_token'])) $params['trust_token'] = (string) $_COOKIE['simbatfa_trust_token']; if (isset($from_password) && false !== $from_password) { // Support login forms that can't be hooked via appending to the password $speculatively_try_appendage = true; $params['two_factor_code'] = $from_password['tfa_code']; } $code_ok = $this->authorise_user_from_login($params, $may_be_email); if (is_wp_error($code_ok)) { $ret = $code_ok; } elseif (!$code_ok) { $encryption_enabled = $this->get_option('tfa_encrypt_secrets'); $additional = ($encryption_enabled && (!defined('SIMBA_TFA_DB_ENCRYPTION_KEY') || '' === SIMBA_TFA_DB_ENCRYPTION_KEY)) ? ' ' . htmlspecialchars(__('The "encrypt secrets" feature is currently enabled, but no encryption key has been found (set via the SIMBA_TFA_DB_ENCRYPTION_KEY constant).', 'all-in-one-wp-security-and-firewall').' '.__('This indicates that either setup failed, or your WordPress installation has been corrupted.', 'all-in-one-wp-security-and-firewall')) . ' '. __('Go here for the FAQs, which explain how a website owner can de-activate the plugin without needing to login.', 'all-in-one-wp-security-and-firewall') .'' : ''; $ret = new WP_Error('authentication_failed', ''.__('Error:', 'all-in-one-wp-security-and-firewall').' '.apply_filters('simba_tfa_message_code_incorrect', __('The one-time password (TFA code) you entered was incorrect.', 'all-in-one-wp-security-and-firewall') . $additional)); if (is_a($user, 'WP_User')) $this->log_incorrect_tfa_code_attempt($user); } elseif ($user) { $ret = $user; } else { if (!empty($speculatively_try_appendage) && true === $code_ok) { $password = $from_password['password']; } $username_is_email = false; if (function_exists('wp_authenticate_username_password') && $may_be_email && filter_var($username, FILTER_VALIDATE_EMAIL)) { global $wpdb; // This has to match self::authorise_user_from_login() $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $username)); if (is_object($response)) $username_is_email = true; } $ret = $username_is_email ? wp_authenticate_email_password(null, $username, $password) : wp_authenticate_username_password(null, $username, $password); } } $ret = apply_filters('simbatfa_verify_code_and_user_result', $ret, $original_user, $username, $password); // If the TFA code was actually validated (not just not required, for example), then $code_ok is (boolean)true if (isset($code_ok) && true === $code_ok && is_a($ret, 'WP_User')) { // Though $_SERVER['SERVER_NAME'] can't always be trusted (if the webserver is misconfigured), anyone using this already has password and TFA clearance. if (!empty($params['simba_tfa_mark_as_trusted']) && $this->user_can_trust($ret->ID) && (is_ssl() || (!empty($_SERVER['SERVER_NAME']) && ('localhost' == $_SERVER['SERVER_NAME'] ||'127.0.0.1' == $_SERVER['SERVER_NAME'] || preg_match('/\.localdomain$/', $_SERVER['SERVER_NAME']))))) { $trusted_for = $this->get_option('tfa_trusted_for'); $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for); $this->trust_device($ret->ID, $trusted_for); } } self::$is_authenticated[$this->authentication_slug] = true; return $ret; } /** * Save incorrect TFA code attempts in database * * @param Array $tfa_incorrect_code_attempts - all user info with incorrent code attempts * @param Boolean $udpate - update in option table * * @retrun Void */ private function save_incorrect_tfa_code_attempts($tfa_incorrect_code_attempts, $update = false) { if ($update) { update_site_option('tfa_incorrect_code_attempts', $tfa_incorrect_code_attempts); } else { add_site_option('tfa_incorrect_code_attempts', $tfa_incorrect_code_attempts); } } /** * Remove old incorrect TFA code attempts * * @param Array $user_info - user invalid attempts * * @retrun Array */ private function remove_incorrect_tfa_code_old_attempts($user_info) { $splice_recs = 0; foreach ($user_info['attempts'] as $attempt) { $mins_diff = (time() - $attempt['activity_time']) / 60; if ($mins_diff >= TFA_INCORRECT_ATTEMPTS_WITHIN_MINUTES_LIMIT) { $splice_recs++; } } if ($splice_recs > 0) array_splice($user_info['attempts'], 0, $splice_recs); // remove all older attempts. return $user_info; } /** * Log incorrect TFA code attempt and email user if attempt exceeded limit * * @param WP_User $user - user object for teh user logging in * * @retrun Void */ private function log_incorrect_tfa_code_attempt($user) { $tfa_incorrect_code_attempts = get_site_option('tfa_incorrect_code_attempts'); if (empty($tfa_incorrect_code_attempts)) $tfa_incorrect_code_attempts = array(); $userinfo_added = false; $update = false; if (count($tfa_incorrect_code_attempts) > 0) { foreach ($tfa_incorrect_code_attempts as $i => $user_info) { $user_info = $this->remove_incorrect_tfa_code_old_attempts($user_info); // remove old (before 30 mins) incorrect tfa code attempts by users if ($user_info['username'] == $user->user_login) { $userinfo_added = true; if (count($user_info['attempts']) >= TFA_INCORRECT_MAX_ATTEMPTS_ALLOWED_LIMIT && empty($user_info['mailsent'])) { $this->notify_incorrect_tfa_code_attempts($user_info, $user->user_email); // if incorrect tfa attempts are more than max allowed notify user by email that some one else has your password. $user_info['mailsent'] = 1; } else { if (0 == count($user_info['attempts'])) $user_info['mailsent'] = 0; $user_info['attempts'][] = $this->get_incorrect_tfa_attempt_info(); //add new incorrect attempt for existing user. } } $tfa_incorrect_code_attempts[$i] = $user_info; } $update = true; } if (false == $userinfo_added) { $tfa_incorrect_code_attempts[] = $this->get_incorrect_tfa_user_info($user); //add incorrect attempt with username etc info. } $this->save_incorrect_tfa_code_attempts($tfa_incorrect_code_attempts, $update); } /** * Get incorrect attempt info time and IP address to save in database * * @retrun Array */ private function get_incorrect_tfa_attempt_info() { $ip_address = apply_filters('tfa_user_ip_address', $_SERVER['REMOTE_ADDR']); return array('activity_time' => time(), 'ip_address' => $ip_address); } /** * Get incorrect attempt with userinfo to save in database * * @param WP_User $user - logging in user object * * @retrun Array */ private function get_incorrect_tfa_user_info($user) { return array('username' => $user->user_login, 'attempts' => array($this->get_incorrect_tfa_attempt_info())); } /** * Notify user might be someone else has your possword * * @param Array $user_info - user's incorrect attempt informaion * @param String $user_email - user email address notification to be sent. */ private function notify_incorrect_tfa_code_attempts($user_info, $user_email) { $subject = __('Incorrect TFA code attempts', 'all-in-one-wp-security-and-firewall'); $email_msg = sprintf(__('There has been an incorrect TFA code entered for logging in to your account %s.', 'all-in-one-wp-security-and-firewall'), $user_info['username']) . "\n\n" . __('Attempts', 'all-in-one-wp-security-and-firewall') . "\n\n"; foreach ($user_info['attempts'] as $index => $attempt) { $email_msg.= ($index+1) . '. ' . wp_date('F j, Y g:i a', $attempt['activity_time'], wp_timezone()) . ' ' . __('from', 'all-in-one-wp-security-and-firewall') . ' ' . $attempt['ip_address']. "\n"; } $email_msg.= "\n" . __('If the above attempts were not by you then someone else has your password.', 'all-in-one-wp-security-and-firewall') . "\n" . __('TFA codes are checked only after the password has been successfully checked.', 'all-in-one-wp-security-and-firewall') . "\n\n" . __('Please change your password urgently.', 'all-in-one-wp-security-and-firewall') . "\n"; $mail_sent = wp_mail($user_email, $subject, $email_msg); } // N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first public function user_can_trust($user_id) { // Default is false because this is a new feature and we don't want to surprise existing users by granting broader access than they expected upon an upgrade return apply_filters('simba_tfa_user_can_trust', false, $user_id); } /** * Should the user be asked for a TFA code? And optionally, is the user allowed to trust devices? * * @param Array $params - the key used is 'log', indicating the username or email address * @param String $response_format - 'simple' (historic format) or 'array' (richer info) * * @return Boolean */ public function pre_auth($params, $response_format = 'simple') { global $wpdb; $query = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']); $user = $wpdb->get_row($query); if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) { // Corner-case: login looks like an email, but is a username rather than email address $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log'])); } $is_activated_for_user = true; $is_activated_by_user = false; $result = false; $totp_controller = $this->get_controller('totp'); if ($user) { $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true); $is_activated_for_user = $this->is_activated_for_user($user->ID); $is_activated_by_user = $this->is_activated_by_user($user->ID); if ($is_activated_for_user && $is_activated_by_user) { // No private key yet, generate one. This shouldn't really be possible. if (!$tfa_priv_key) $tfa_priv_key = $totp_controller->addPrivateKey($user->ID); $code = $totp_controller->generateOTP($user->ID, $tfa_priv_key); $result = true; } } if ('array' != $response_format) return $result; $ret = array('result' => $result); if ($result) { $ret['user_can_trust'] = $this->user_can_trust($user->ID); if (!empty($params['trust_token']) && $this->user_trust_token_valid($user->ID, $params['trust_token'])) { $ret['user_already_trusted'] = 1; } } return $ret; } /** * Print the radio buttons for enabling/disabling TFA * * @param Integer $user_id - the WordPress user ID * @param Boolean $long_label - whether to use a long label rather than a short one * @param String $style - valid values are "show_current" and "require_current" */ public function paint_enable_tfa_radios($user_id, $long_label = false, $style = 'show_current') { if (!$user_id) return; if ('require_current' != $style) $style = 'show_current'; $is_required = $this->is_required_for_user($user_id); $is_activated = $this->is_activated_by_user($user_id); if ($is_required) { $require_after = absint($this->get_option('tfa_requireafter')); echo '
'.sprintf(__('N.B. This site is configured to forbid you to log in if you disable two-factor authentication after your account is %d days old', 'all-in-one-wp-security-and-firewall'), $require_after).'
'; } $tfa_enabled_label = $long_label ? __('Enable two-factor authentication', 'all-in-one-wp-security-and-firewall') : __('Enabled', 'all-in-one-wp-security-and-firewall'); if ('show_current' == $style) { $tfa_enabled_label .= ' '.sprintf(__('(Current code: %s)', 'all-in-one-wp-security-and-firewall'), $this->get_controller('totp')->current_otp_code($user_id)); } elseif ('require_current' == $style) { $tfa_enabled_label .= ' '.sprintf(__('(you must enter the current code: %s)', 'all-in-one-wp-security-and-firewall'), ''); } $show_disable = ((is_multisite() && is_super_admin()) || (!is_multisite() && current_user_can($this->get_management_capability())) || false == $is_activated || !$is_required || !$this->get_option('tfa_hide_turn_off')) ? true : false; $tfa_disabled_label = $long_label ? __('Disable two-factor authentication', 'all-in-one-wp-security-and-firewall') : __('Disabled', 'all-in-one-wp-security-and-firewall'); if ('require_current' == $style) echo ''."\n"; echo '