false, 'service.secure_login.brute_force_lockout' => false, 'service.secure_login.track_last_access' => true, 'service.secure_login.time_window' => '+20 minutes', 'service.secure_login.login_attempts' => 5, 'service.secure_login.login_message' => 'Login to get access.' ]; /** * Constructor * * @return void * @access protected * * @version 7.0.6 */ protected function __construct() { add_filter('aam_get_config_filter', function($result, $key) { if (empty($result) && array_key_exists($key, self::DEFAULT_CONFIG)) { $result = self::DEFAULT_CONFIG[$key]; } return $result; }, 10, 2); // Register custom RESTful API endpoint for login AAM_Restful_SecureLogin::bootstrap(); // Register custom frontend Login widget add_action('widgets_init', function () { register_widget('AAM_Backend_Widget_Login'); }); // Register the equivalent block for the block and site editors. add_action('init', [ AAM_Backend_Block_SecureLogin::class, 'register' ]); add_action('init', function() { $this->initialize_hooks(); }, PHP_INT_MAX); } /** * Initialize core hooks * * @return void * @access protected * * @version 8.0.2 */ protected function initialize_hooks() { $this->_initialize_tracker(); add_action('aam_initialize_ui_action', function () { AAM_Backend_Feature_Settings_Security::register(); }); // Redefine the wp-login.php header message add_filter('login_message', function($message) { return $this->_login_message($message); }); // Security controls add_filter('authenticate', function($response) { return $this->_authenticate($response); }, -9999); add_filter('auth_cookie', function($cookie, $user_id, $_, $__, $token) { return $this->_auth_cookie($cookie, $user_id, $token); }, 10, 5); add_action('wp_login_failed', function() { $this->_wp_login_failed(); }); if (is_user_logged_in()) { $this->_record_last_access(wp_get_current_user()); } } /** * Establish the first reliable day of last-user-access tracking * * @return void * @access private * * @version 8.0.2 */ private function _initialize_tracker() { if (AAM::api()->config->get('service.secure_login.track_last_access') && get_option(self::TRACKER_STARTED_OPTION, null) === null ) { add_option(self::TRACKER_STARTED_OPTION, time(), '', true); } } /** * Record the most recent successful login in UTC. * * @param WP_User $user * */ private function _record_last_access($user) { $configs = AAM::api()->config; $enabled = $configs->get('service.secure_login.track_last_access'); if ($enabled) { update_user_meta( $user->ID, self::LAST_ACCESS_TIME_META, current_time('mysql', true) ); $ip = apply_filters( 'aam_get_user_ip_address_filter', $_SERVER['REMOTE_ADDR'] ?? '' ); if (is_string($ip) && filter_var($ip, FILTER_VALIDATE_IP)) { update_user_meta($user->ID, self::LAST_ACCESS_IP_META, $ip); } } } /** * Intercept auth token generation and enhance security * * If "One Session Per User" option is enabled, make sure that all other sessions * are removed * * @param string $cookie Authentication cookie. * @param int $user_id User ID. * @param string $token User's session token used. * * @return string * @access private * * @version 7.0.0 */ private function _auth_cookie($cookie, $user_id, $token) { $configs = AAM::api()->config; // Remove all other sessions if single session feature is enabled if ($configs->get('service.secure_login.single_session')) { $sessions = WP_Session_Tokens::get_instance($user_id); if (count($sessions->get_all()) > 1) { $sessions->destroy_others($token); } } return $cookie; } /** * Track failed login attempts * * This method is used to enable brute force protection * * @return void * @access private * * @version 7.0.0 */ private function _wp_login_failed() { $configs = AAM::api()->config; $enabled = $configs->get('service.secure_login.brute_force_lockout'); // Track failed attempts only if Brute Force Lockout is enabled if ($enabled) { $name = $this->_get_login_attempt_key(); $attempts = AAM::api()->cache->get($name); if (!empty($attempts)) { $attempts = intval($attempts) + 1; AAM::api()->cache->update($name, $attempts); } else { $timeout = strtotime($configs->get( 'service.secure_login.time_window' )); AAM::api()->cache->set($name, 1, $timeout - time()); } } } /** * Pre-authentication hook * * Enhance authentication security with Brute Force protection or login delay * * @param mixed $response * * @return mixed * @access private * @see wp_authenticate * * @version 7.0.0 */ private function _authenticate($response) { $configs = AAM::api()->config; // Brute Force Lockout if ($configs->get('service.secure_login.brute_force_lockout')) { $threshold = $configs->get('service.secure_login.login_attempts'); $attempts = AAM::api()->cache->get($this->_get_login_attempt_key()); if ($attempts >= $threshold) { // To ensure all ther authentication handlers are skipped, we // return WP_User object with user ID of 0. This will be treated // as a failed login attempt $response = new WP_User(0); // $response = new WP_Error(405, // __('Exceeded maximum number for login attempts.', 'advanced-access-manager') // ); } } return $response; } /** * Customize login message * * @param string $message * * @return string * @access private * * @version 7.0.0 */ private function _login_message($message) { $reason = AAM::api()->misc->get($_GET, 'reason'); if (empty($message) && ($reason === 'restricted')) { $str = AAM::api()->config->get( 'service.secure_login.login_message' ); $message = '

' . esc_js($str) . '

'; } return $message; } /** * Get login attempts counter key name * * @return string * @access private * * @version 7.0.0 */ private function _get_login_attempt_key() { $whip = new Whip(); return 'failed_login_attempts_' . $whip->getValidIpAddress(); } }