return $this->user_settings_page_slug; } /** * Set settings page heading for plugin's option page * * @param String $settings_page_heading String. * * @return String */ public function set_settings_page_heading($settings_page_heading) { $this->settings_page_heading = $settings_page_heading; } /** * Get settings page heading for plugin's option page. * * @return String Setting page heading. */ public function get_settings_page_heading() { return $this->settings_page_heading; } /** * Set plugin translate url * * @param String $plugin_translate_url Plugin translation URL. * @return Void */ public function set_plugin_translate_url($plugin_translate_url) { $this->plugin_translate_url = $plugin_translate_url; } /** * Get plugin translate url * * @return String Plugin translate URL */ public function get_plugin_translate_url() { return $this->plugin_translate_url; } /** * Set plugin premium version url * * @param String $premium_version_url Plugin premium version url. * @return Void */ public function set_premium_version_url($premium_version_url) { $this->premium_version_url = $premium_version_url; } /** * Get plugin premium version URL. * * @return String Plugin premium version URL. */ public function get_premium_version_url() { return $this->premium_version_url; } /** * Set plugin FAQ URL * * @param String $faq_url Plugin FAQ URL. * @return Void */ public function set_faq_url($faq_url) { $this->faq_url = $faq_url; } /** * Get plugin FAQ URL. * * @return String Plugin FAQ URL. */ public function get_faq_url() { return $this->faq_url; } /** * Set plugin site wide administration URL * * @param String $site_wide_administration_url Plugin site wide administration URL. * @return Void */ public function set_site_wide_administration_url($site_wide_administration_url) { $this->site_wide_administration_url = $site_wide_administration_url; } /** * Get plugin site wide administration URL. * * @return String Plugin site wide administration URL */ public function get_site_wide_administration_url() { return $this->site_wide_administration_url; } /** * Give the filesystem path to the plugin's templates directory * * @return String */ public function templates_dir() { return __DIR__.'/templates'; } /** * Include the user settings page code */ public function show_dashboard_user_settings_page() { $this->include_template('user-settings.php'); } /** * Enqueue CSS styling on the users page */ public function load_users_css() { $css_version = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/users.css'); wp_enqueue_style( 'tfa-users-css', $this->includes_url().'/users.css', array(), $css_version, 'screen' ); } /** * Add the 2FA label to the users list table header. * * @param Array $columns Table columns. * * @return Array */ public function manage_users_columns_tfa($columns = array()) { $columns['tfa-status'] = __('2FA', 'all-in-one-wp-security-and-firewall'); return $columns; } /** * Add status into TFA column. * * @param String $value String. * @param String $column_name Column name. * @param Integer $user_id User ID. * * @return String */ public function manage_users_custom_column_tfa($value = '', $column_name = '', $user_id = 0) { // Only for this column name. if ('tfa-status' === $column_name) { if (!$this->is_activated_for_user($user_id)) { $value = '—'; } elseif ($this->is_activated_by_user($user_id)) { // Use value. $value = ''; } else { // No group. $value = ''; } } return $value; } /** * Paint out an admin notice * * @param String $message - the caller should already have taken care of any escaping * @param String $class */ public function show_admin_warning($message, $class = 'updated') { echo '
"; } /** * Returns all two factor authentication setting name => group pairs. * * @return Array */ private function get_config_keys() { global $wp_roles; if (!isset($wp_roles)) $wp_roles = new WP_Roles(); $keys = array( 'tfa_requireafter' => 'tfa_user_roles_required_group', 'tfa_require_enforce_after' => 'tfa_user_roles_required_group', 'tfa_if_required_redirect_to' => 'tfa_user_roles_required_group', 'tfa_hide_turn_off' => 'tfa_user_roles_required_group', 'tfa_trusted_for' => 'tfa_user_roles_trusted_group', 'tfa_wc_add_section' => 'simba_tfa_woocommerce_group', 'tfa_bot_protection' => 'simba_tfa_woocommerce_group', 'tfa_default_hmac' => 'simba_tfa_default_hmac_group', 'tfa_encrypt_secrets' => 'simba_tfa_encrypt_secrets_group', 'tfa_xmlrpc_on' => 'tfa_xmlrpc_status_group', ); foreach ($wp_roles->role_names as $id => $name) { $keys['tfa_'.$id] = 'tfa_user_roles_group'; $keys['tfa_trusted_'.$id] = 'tfa_user_roles_trusted_group'; $keys['tfa_required_'.$id] = 'tfa_user_roles_required_group'; } if (is_multisite()) { $keys['tfa__super_admin'] = 'tfa_user_roles_group'; $keys['tfa_trusted__super_admin'] = 'tfa_user_roles_trusted_group'; $keys['tfa_required__super_admin'] = 'tfa_user_roles_required_group'; } return $keys; } /** * Registers all two factor authentication settings. Runs upon the WP action admin_init. */ public function register_two_factor_auth_settings() { $config_keys = $this->get_config_keys(); foreach ($config_keys as $name => $group) { register_setting($group, $name); } } /** * Returns all two factor authentication options from the WP database. * * @return Array */ public function get_configs() { $config_keys = $this->get_config_keys(); $configs = array(); foreach (array_keys($config_keys) as $name) { if (false !== $this->get_option($name)) { $configs[$name] = $this->get_option($name); } } return $configs; } /** * Sets two factor authentication options from array. * * @param Array $configs * * @return Boolean */ public function set_configs($configs) { $result = false; foreach ($configs as $key => $value) { $result = $this->update_option($key, $value) ? true : $result; } return $result; } /** * Deletes all two factor authentication options from the WP database. * * @return Void */ public function delete_configs() { $config_keys = $this->get_config_keys(); foreach (array_keys($config_keys) as $name) { $this->delete_option($name); } } /** * See whether TFA is available or not for a particular user - i.e. whether the administrator has permitted it for their user level * * @param Integer $user_id - WordPress user ID * * @return Boolean */ public function is_activated_for_user($user_id) { if (empty($user_id)) return false; // Super admin is not a role (they are admins with an extra attribute); needs separate handling if (is_multisite() && is_super_admin($user_id)) { // This is always a final decision - we don't want it to drop through to the 'admin' role's setting $role = '_super_admin'; $db_val = $this->get_option('tfa_'.$role); // Defaults to true if no setting has been saved return (false === $db_val || $db_val) ? true : false; } $roles = $this->get_user_roles($user_id); // N.B. This populates with roles on the current site within a multisite foreach ($roles as $role) { $db_val = $this->get_option('tfa_'.$role); if (false === $db_val || $db_val) return true; } return false; } /** * Get all user roles for a given user (if on multisite, amalgamates all roles from all sites) * * @param Integer $user_id - WordPress user ID * * @return Array */ protected function get_user_roles($user_id) { // Get roles on the main site $user = new WP_User($user_id); $roles = (array) $user->roles; // On multisite, also check roles on non-main sites if (is_multisite()) { global $wpdb, $table_prefix; $roles_db = $wpdb->get_results($wpdb->prepare("SELECT meta_key, meta_value FROM {$wpdb->usermeta} WHERE user_id=%d AND meta_key LIKE '".esc_sql($table_prefix)."%_capabilities'", $user_id)); if (is_array($roles_db)) { foreach ($roles_db as $role_info) { if (empty($role_info->meta_key) || !preg_match('/^'.$table_prefix.'\d+_capabilities$/', $role_info->meta_key) || empty($role_info->meta_value) || !preg_match('/^a:/', $role_info->meta_value)) continue; $site_roles = unserialize($role_info->meta_value); if (!is_array($site_roles)) continue; foreach ($site_roles as $role => $active) { if ($active && !in_array($role, $roles)) $roles[] = $role; } } } } return $roles; } /** * Check if TFA is required for a specified user * * N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first * * @param $user_id Integer - the WP user ID * * @return Boolean */ public function is_required_for_user($user_id) { return apply_filters('simba_tfa_required_for_user', $this->user_property_active($user_id, 'required_'), $user_id); } /** * See if a particular user property is active * * @param Integer $user_id * @param String $prefix - e.g. "required_", "trusted_" * * @return Boolean */ public function user_property_active($user_id, $prefix = 'required_') { if (empty($user_id)) return false; // Super admin is not a role (they are admins with an extra attribute); needs separate handling if (is_multisite() && is_super_admin($user_id)) { // This is always a final decision - we don't want it to drop through to the 'admin' role's setting $role = '_super_admin'; $db_val = $this->get_option('tfa_'.$prefix.$role); return $db_val ? true : false; } $roles = $this->get_user_roles($user_id); foreach ($roles as $role) { $db_val = $this->get_option('tfa_'.$prefix.$role); if ($db_val) return true; } return false; } /** * Whether TFA is activated by a specific user. Note that this doesn't check if TFA is enabled for the user's role; the caller should check that first. * * @param Integer $user_id * * @return Boolean */ public function is_activated_by_user($user_id) { $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true); return !empty($enabled); } /** * Get a list of trusted devices for the user * * @param Integer|Boolean $user_id - WordPress user ID, or false for the current user * * @return Array */ public function user_get_trusted_devices($user_id = false) { if (false === $user_id) { global $current_user; $user_id = $current_user->ID; } $trusted_devices = get_user_meta($user_id, 'tfa_trusted_devices', true); if (!is_array($trusted_devices)) $trusted_devices = array(); return $trusted_devices; } /** * Trust the current device * * @param Integer $user_id - WordPress user ID * @param Integer $trusted_for - time to trust for, in days */ public function trust_device($user_id, $trusted_for) { $trusted_devices = $this->user_get_trusted_devices($user_id); $time_now = time(); foreach ($trusted_devices as $k => $device) { if (empty($device['until']) || $device['until'] <= $time_now) unset($trusted_devices[$k]); } $until = $time_now + $trusted_for * 86400; $token = bin2hex($this->random_bytes(40)); $trusted_devices[] = array( 'ip' => $_SERVER['REMOTE_ADDR'], 'until' => $until, 'user_agent' => empty($_SERVER['HTTP_USER_AGENT']) ? '' : (string) $_SERVER['HTTP_USER_AGENT'], 'token' => $token ); $this->user_set_trusted_devices($user_id, $trusted_devices); $this->set_cookie('simbatfa_trust_token', $token, $until); } /** * Returns true if running on a PHP version on which mcrypt has been deprecated * * @return Boolean */ public function is_mcrypt_deprecated() { return (7 == PHP_MAJOR_VERSION && PHP_MINOR_VERSION >= 1); } /** * Return the specified number of bytes * * @param Integer $bytes * * @throws Exception * * @return String */ public function random_bytes($bytes) { if (function_exists('random_bytes')) { return random_bytes($bytes); } elseif (function_exists('mcrypt_create_iv')) { return $this->is_mcrypt_deprecated() ? @mcrypt_create_iv($bytes, MCRYPT_RAND) : mcrypt_create_iv($bytes, MCRYPT_RAND); } elseif (function_exists('openssl_random_pseudo_bytes')) { return openssl_random_pseudo_bytes($bytes); } throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed'); } /** * Set a cookie so that, however we logged in, it can be found * * @param String $name - the cookie name * @param String $value - the cookie value * @param Integer $expires - when the cookie expires, in epoch time. Defaults to 24 hours' time. Values in the past cause cookie deletion. */ protected function set_cookie($name, $value, $expires = null) { if (null === $expires) $expires = time() + 86400; $secure = is_ssl(); $secure_logged_in_cookie = ($secure && 'https' === parse_url(get_option('home'), PHP_URL_SCHEME)); $secure = apply_filters('secure_auth_cookie', $secure, get_current_user_id()); $secure_logged_in_cookie = apply_filters('secure_logged_in_cookie', $secure_logged_in_cookie, get_current_user_id(), $secure); setcookie($name, $value, $expires, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, $secure, true); setcookie($name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true); if (COOKIEPATH != SITECOOKIEPATH) { setcookie($name, $value, $expires, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true); } } /** * Get a list of trusted devices for the user * * @param Integer $user_id - WordPress user ID * @param Array $trusted_devices - the list of devices */ public function user_set_trusted_devices($user_id, $trusted_devices) { update_user_meta($user_id, 'tfa_trusted_devices', $trusted_devices); } /** * Get the user capability needed for managing TFA users. * You'll want to think carefully about changing this to a non-admin, as it can give the ability to lock admins out (though, if you have FTP/files access, you can always disable TFA or any plugin) * * @return String */ public function get_management_capability() { return apply_filters('simba_tfa_management_capability', 'manage_options'); } /** * Used with set_error_handler() * * @param Integer $errno * @param String $errstr * @param String $errfile * @param Integer $errline * * @return Boolean */ public function get_php_errors($errno, $errstr, $errfile, $errline) { if (0 == error_reporting()) return true; $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline); $this->logged[] = $logline; # Don't pass it up the chain (since it's going to be output to the user always) return true; } public function php_error_to_logline($errno, $errstr, $errfile, $errline) { switch ($errno) { case 1: $e_type = 'E_ERROR'; break; case 2: $e_type = 'E_WARNING'; break; case 4: $e_type = 'E_PARSE'; break; case 8: $e_type = 'E_NOTICE'; break; case 16: $e_type = 'E_CORE_ERROR'; break; case 32: $e_type = 'E_CORE_WARNING'; break; case 64: $e_type = 'E_COMPILE_ERROR'; break; case 128: $e_type = 'E_COMPILE_WARNING'; break; case 256: $e_type = 'E_USER_ERROR'; break; case 512: $e_type = 'E_USER_WARNING'; break; case 1024: $e_type = 'E_USER_NOTICE'; break; case 2048: $e_type = 'E_STRICT'; break; case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break; case 8192: $e_type = 'E_DEPRECATED'; break; case 16384: $e_type = 'E_USER_DEPRECATED'; break; case 30719: $e_type = 'E_ALL'; break; default: $e_type = "E_UNKNOWN ($errno)"; break; } if (!is_string($errstr)) $errstr = serialize($errstr); if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH)); return "PHP event: code $e_type: $errstr (line $errline, $errfile)"; } /** * Runs upon the WordPress 'init' action */ public function init() { 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 '