PluginProbe
Advanced Access Manager – Access Governance for WordPress / 6.9.0
Advanced Access Manager – Access Governance for WordPress v6.9.0
7.1.4 7.1.2 7.1.3 6.8.4 6.8.5 6.9.0 6.9.1 6.9.10 6.9.11 6.9.12 6.9.13 6.9.14 6.9.15 6.9.16 6.9.17 6.9.18 6.9.19 6.9.2 6.9.20 6.9.21 6.9.22 6.9.23 6.9.24 6.9.25 6.9.26 All 210 releases
advanced-access-manager / application / Service / SecureLogin.php
SecureLogin.php
620 lines 18.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * ======================================================================
5 * LICENSE: This file is subject to the terms and conditions defined in *
6 * file 'license.txt', which is part of this source code package. *
7 * ======================================================================
8 */
9
10 /**
11 * Secure Login service
12 *
13 * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
14 * @since 6.6.1 https://github.com/aamplugin/advanced-access-manager/issues/136
15 * @since 6.4.2 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/91
16 * @since 6.4.0 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/16.
17 * Enhanced https://github.com/aamplugin/advanced-access-manager/issues/71
18 * @since 6.3.1 Fixed bug with not being able to lock user
19 * @since 6.1.0 Enriched error response with more details
20 * @since 6.0.0 Initial implementation of the class
21 *
22 * @package AAM
23 * @version 6.6.2
24 */
25 class AAM_Service_SecureLogin
26 {
27 use AAM_Core_Contract_RequestTrait,
28 AAM_Core_Contract_ServiceTrait;
29
30 /**
31 * Service alias
32 *
33 * Is used to get service instance if it is enabled
34 *
35 * @version 6.4.0
36 */
37 const SERVICE_ALIAS = 'secure-login';
38
39 /**
40 * AAM configuration setting that is associated with the service
41 *
42 * @version 6.0.0
43 */
44 const FEATURE_FLAG = 'core.service.secure-login.enabled';
45
46 /**
47 * Constructor
48 *
49 * @return void
50 *
51 * @access protected
52 * @version 6.0.0
53 */
54 protected function __construct()
55 {
56 if (is_admin()) {
57 // Hook that returns the detailed information about the nature of the
58 // service. This is used to display information about service on the
59 // Settings->Services tab
60 add_filter('aam_service_list_filter', function ($services) {
61 $services[] = array(
62 'title' => __('Secure Login', AAM_KEY),
63 'description' => __('Enhance default WordPress authentication process with more secure login mechanism. The service registers frontend AJAX Login widget as well as additional endpoints for the RESTful API authentication.', AAM_KEY),
64 'setting' => self::FEATURE_FLAG
65 );
66
67 return $services;
68 }, 1);
69
70 // Register additional tab for the Settings
71 if (AAM_Core_Config::get(self::FEATURE_FLAG, true)) {
72 add_action('aam_init_ui_action', function () {
73 AAM_Backend_Feature_Settings_Security::register();
74 }, 1);
75 }
76 }
77
78 if (AAM_Core_Config::get(self::FEATURE_FLAG, true)) {
79 $this->initializeHooks();
80 }
81 }
82
83 /**
84 * Initialize core hooks
85 *
86 * @return void
87 *
88 * @since 6.4.0 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/71
89 * @since 6.0.0 Initial implementation of the method
90 *
91 * @access protected
92 * @version 6.4.0
93 */
94 protected function initializeHooks()
95 {
96 // Register custom frontend Login widget
97 add_action('widgets_init', function () {
98 register_widget('AAM_Backend_Widget_Login');
99 });
100
101 // Register custom RESTful API endpoint for login
102 add_action('rest_api_init', array($this, 'registerRESTfulRoute'));
103
104 // User login control
105 add_filter('wp_authenticate_user', array($this, 'validateUserStatus'), 1, 2);
106 add_filter('aam_verify_user_filter', array($this, 'validateUserStatus'));
107
108 // Redefine the wp-login.php header message
109 add_filter('login_message', array($this, 'loginMessage'));
110
111 // Security controls
112 add_filter('authenticate', array($this, 'enhanceAuthentication'), PHP_INT_MAX);
113 add_filter('auth_cookie', array($this, 'manageAuthCookie'), 10, 5);
114 add_action('wp_login_failed', array($this, 'trackFailedLoginAttempt'));
115
116 // AAM UI controls
117 add_filter('aam_user_row_actions_filter', function($actions, $user) {
118 // Move this to the Secure Login Service
119 if (current_user_can('aam_toggle_users')) {
120 $actions[] = ($user->user_status ? 'unlock' : 'lock');
121 }
122
123 return $actions;
124 }, 10, 2);
125 add_filter('aam_ajax_filter', array($this, 'handleAjax'), 10, 3);
126 add_filter('aam_user_expiration_actions_filter', function($actions) {
127 $actions['lock'] = __('Block User Account', AAM_KEY);
128
129 return $actions;
130 });
131 add_action('aam_process_inactive_user_action', array($this, 'lockUser'), 10, 2);
132
133 // AAM Core integration
134 add_action('aam_initialize_user_action', function(AAM_Core_Subject_User $user) {
135 $currentId = get_current_user_id();
136
137 if (intval($user->user_status) === 1 && ($currentId === $user->ID)) {
138 wp_logout();
139 }
140 });
141
142 // Service fetch
143 $this->registerService();
144 }
145
146 /**
147 * Register RESTful Route
148 *
149 * Register AAM authentication endpoint
150 *
151 * @since 6.6.1 https://github.com/aamplugin/advanced-access-manager/issues/136
152 * @since 6.4.2 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/91
153 * @since 6.0.0 Initial implementation of the method
154 *
155 * @return void
156 * @version 6.6.1
157 */
158 public function registerRESTfulRoute()
159 {
160 $config = array(
161 'methods' => 'POST',
162 'callback' => array($this, 'authenticate'),
163 'permission_callback' => '__return_true',
164 'args' => apply_filters('aam_restful_authentication_args_filter', array(
165 'username' => array(
166 'description' => 'Valid username.',
167 'type' => 'string',
168 ),
169 'password' => array(
170 'description' => 'Valid password.',
171 'type' => 'string',
172 ),
173 'redirect' => array(
174 'description' => 'Redirect URL after authentication.',
175 'type' => 'string',
176 ),
177 'remember' => array(
178 'description' => 'Prolong the user session.',
179 'type' => 'boolean',
180 ),
181 'returnAuthCookies' => array(
182 'description' => 'Return auth cookies.',
183 'type' => 'boolean',
184 )
185 )),
186 );
187
188 register_rest_route('aam/v2', '/authenticate', $config);
189
190 // For backward compatibility, keep /v1/authenticate endpoint
191 register_rest_route('aam/v1', '/authenticate', array(
192 'methods' => 'POST',
193 'callback' => array($this, 'legacyAuthenticate'),
194 'permission_callback' => '__return_true',
195 'args' => array(
196 'username' => array(
197 'description' => __('Valid username.', AAM_KEY),
198 'type' => 'string',
199 ),
200 'password' => array(
201 'description' => __('Valid password.', AAM_KEY),
202 'type' => 'string',
203 )
204 ),
205 ));
206 }
207
208 /**
209 * Authenticate user
210 *
211 * @param WP_REST_Request $request
212 *
213 * @return WP_REST_Response
214 *
215 * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
216 * @since 6.4.2 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/91
217 * @since 6.4.0 Enhanced https://github.com/aamplugin/advanced-access-manager/issues/16
218 * @since 6.1.0 Enriched error response with more details
219 * @since 6.0.0 Initial implementation of the method
220 *
221 * @access public
222 * @version 6.6.2
223 */
224 public function authenticate(WP_REST_Request $request)
225 {
226 $status = 200;
227
228 // No need to generate Auth cookies, unless explicitly stated so
229 if ($request->get_param('returnAuthCookies') !== true) {
230 add_filter('send_auth_cookies', '__return_false');
231 }
232
233 $user = wp_signon(array(
234 'user_login' => $request->get_param('username'),
235 'user_password' => $request->get_param('password'),
236 'remember' => $request->get_param('remember')
237 ));
238
239 try {
240 if (!is_wp_error($user)) {
241 $result = apply_filters('aam_auth_response_filter', array(
242 'user' => $this->prepareUserData($user),
243 'redirect' => $request->get_param('redirect')
244 ), $request, $user);
245 } else {
246 $status = 403;
247 $result = array(
248 'code' => $user->get_error_code(),
249 'reason' => $user->get_error_message()
250 );
251 }
252 } catch(Exception $e) {
253 $status = $e->getCode();
254 $result = array(
255 'reason' => $e->getMessage()
256 );
257 }
258
259 return new WP_REST_Response($result, $status);
260 }
261
262 /**
263 * Authenticate user
264 *
265 * @param WP_REST_Request $request
266 *
267 * @return WP_REST_Response
268 *
269 * @since 6.6.2 https://github.com/aamplugin/advanced-access-manager/issues/139
270 * @since 6.4.2 Initial implementation of the method
271 *
272 * @access public
273 * @version 6.6.2
274 */
275 public function legacyAuthenticate(WP_REST_Request $request)
276 {
277 _deprecated_function('aam/v1/authenticate', '6.4.2', 'aam/v2/authenticate');
278
279 $user = wp_signon(array(
280 'user_login' => $request->get_param('username'),
281 'user_password' => $request->get_param('password')
282 ));
283
284 if (is_a($user, 'WP_User')) {
285 $status = 200;
286
287 // Making sure that token is issued
288 $request->set_param('issueJWT', true);
289
290 $result = apply_filters(
291 'aam_auth_response_filter',
292 array('user' => $this->prepareUserData($user)),
293 $request,
294 $user
295 );
296 } else {
297 $status = 403;
298 $result = new WP_Error(
299 'rest_jwt_auth_failure',
300 strip_tags($user->get_error_message())
301 );
302 }
303
304 return new WP_REST_Response($result, $status);
305 }
306
307 /**
308 * Prepare user data that is returned
309 *
310 * @param WP_User $user
311 *
312 * @return array
313 *
314 * @access protected
315 * @version 6.6.2
316 */
317 protected function prepareUserData($user) {
318 $response = array('data' => array());
319
320 $props = array(
321 'ID', 'user_login', 'user_nicename', 'display_name', 'user_url',
322 'user_email', 'user_registered'
323 );
324
325 foreach($props as $prop) {
326 $response['data'][$prop] = $user->{$prop};
327 }
328
329 return $response;
330 }
331
332 /**
333 * Intercept auth token generation and enhance security
334 *
335 * If "One Session Per User" option is enabled, make sure that all other sessions
336 * are removed
337 *
338 * @param string $cookie Authentication cookie.
339 * @param int $user_id User ID.
340 * @param int $expiration The time the cookie expires as a UNIX timestamp.
341 * @param string $scheme Cookie scheme used. Accepts 'auth', 'secure_auth', or 'logged_in'.
342 * @param string $token User's session token used.
343 *
344 * @return string
345 *
346 * @access public
347 * @version 6.0.0
348 */
349 public function manageAuthCookie($cookie, $user_id, $expiration, $scheme, $token)
350 {
351 // Remove all other sessions if single session feature is enabled
352 if (AAM_Core_Config::get('service.secureLogin.feature.singleSession', false)) {
353 $sessions = WP_Session_Tokens::get_instance($user_id);
354
355 if (count($sessions->get_all()) > 1) {
356 $sessions->destroy_others($token);
357 }
358 }
359
360 return $cookie;
361 }
362
363 /**
364 * Track failed login attempts
365 *
366 * This method is used to enable brute force protection
367 *
368 * @return void
369 *
370 * @access public
371 * @version 6.0.0
372 */
373 public function trackFailedLoginAttempt()
374 {
375 // Track failed attempts only if Brute Force Lockout is enabled
376 if (AAM_Core_Config::get('service.secureLogin.feature.bruteForceLockout', false)) {
377 $this->updateLoginAttemptsTransient(1);
378 }
379 }
380
381 /**
382 * Increment/Decrement failed login attempts transient
383 *
384 * @param int $counter
385 *
386 * @return void
387 *
388 * @access protected
389 * @version 6.0.0
390 */
391 protected function updateLoginAttemptsTransient($counter)
392 {
393 $name = $this->getLoginAttemptTransientName();
394 $attempts = get_transient($name);
395
396 if ($attempts !== false) {
397 $timeout = get_option("_transient_timeout_{$name}");
398 $attempts = intval($attempts) + $counter;
399 } else {
400 $attempts = 1;
401 $timeout = strtotime(
402 AAM_Core_Config::get(
403 'service.secureLogin.settings.attemptWindow',
404 '20 minutes'
405 )
406 );
407 }
408
409 set_transient($name, $attempts, $timeout - time());
410 }
411
412 /**
413 * Get login attempts transient name
414 *
415 * @return string
416 *
417 * @access private
418 * @version 6.0.0
419 */
420 private function getLoginAttemptTransientName()
421 {
422 return sprintf(
423 'aam_failed_login_attempts_%s', $this->getFromServer('REMOTE_ADDR')
424 );
425 }
426
427 /**
428 * Pre-authentication hook
429 *
430 * Enhance authentication security with Brute Force protection or login delay
431 *
432 * @param mixed $response
433 *
434 * @return mixed
435 *
436 * @access public
437 * @see wp_authenticate
438 * @version 6.0.0
439 */
440 public function enhanceAuthentication($response)
441 {
442 // Brute Force Lockout
443 if (AAM_Core_Config::get('service.secureLogin.feature.bruteForceLockout', false)) {
444 $attempts = get_transient($this->getLoginAttemptTransientName());
445 $threshold = AAM_Core_Config::get('service.secureLogin.settings.loginAttempts', 20);
446
447 if ($attempts >= $threshold) {
448 $response = new WP_Error(
449 405,
450 __('Exceeded maximum number for authentication attempts. Try again later.', AAM_KEY)
451 );
452 }
453 }
454
455 return $response;
456 }
457
458 /**
459 * Validate user status
460 *
461 * Check if user is locked or not
462 *
463 * @param WP_Error $user
464 *
465 * @return WP_Error|WP_User
466 *
467 * @access public
468 * @version 6.0.0
469 */
470 public function validateUserStatus($user)
471 {
472 // Check if user is blocked
473 if (is_a($user, 'WP_User') && (intval($user->user_status) === 1)) {
474 $user = new WP_Error(
475 405,
476 AAM_Backend_View_Helper::preparePhrase(
477 '[ERROR]: User is locked. Contact website administrator.',
478 'strong'
479 )
480 );
481 }
482
483 return $user;
484 }
485
486 /**
487 * Customize login message
488 *
489 * @param string $message
490 *
491 * @return string
492 *
493 * @access public
494 * @version 6.0.0
495 */
496 public function loginMessage($message)
497 {
498 if (empty($message) && ($this->getFromQuery('reason') === 'restricted')) {
499 $message = sprintf(
500 __('%sAccess is restricted. Login to get access.%s', AAM_KEY),
501 '<p class="message">',
502 '</p>'
503 );
504 }
505
506 return $message;
507 }
508
509 /**
510 * Handle AAM UI ajax calls
511 *
512 * @param mixed $response
513 * @param AAM_Core_Subject_User $user
514 * @param string $action
515 *
516 * @return mixed
517 *
518 * @since 6.3.1 Fixed bug https://github.com/aamplugin/advanced-access-manager/issues/43
519 * @since 6.0.0 Initial implementation of the method
520 *
521 * @access public
522 * @version 6.3.1
523 */
524 public function handleAjax($response, $user, $action)
525 {
526 if ($action === 'Service_SecureLogin.toggleUserStatus') {
527 $result = $this->toggleUserStatus($user);
528 $response = wp_json_encode(
529 array('status' => ($result ? 'success' : 'failure'))
530 );
531 }
532
533 return $response;
534 }
535
536 /**
537 * Lock user
538 *
539 * This method is invoked when user is expired
540 *
541 * @param array $trigger
542 * @param AAM_Core_Subject_User $user
543 *
544 * @return void
545 *
546 * @access public
547 * @version 6.0.0
548 */
549 public function lockUser(array $trigger, AAM_Core_Subject_User $user)
550 {
551 if ($trigger['action'] === 'lock') {
552 $this->changeUserStatus($user->getPrincipal(), 1);
553 wp_logout();
554 }
555 }
556
557 /**
558 * Toggle user status
559 *
560 * Either block or unblock user record
561 *
562 * @param AAM_Core_Subject_User $user
563 *
564 * @return void
565 *
566 * @access protected
567 * @version 6.0.0
568 */
569 protected function toggleUserStatus(AAM_Core_Subject_User $user)
570 {
571 $result = false;
572
573 if (current_user_can('aam_toggle_users') && current_user_can('edit_users')) {
574 if (apply_filters('aam_user_can_manage_level_filter', true, $user->getMaxLevel())) {
575 // User is not allowed to lock himself
576 if (intval($user->getId()) !== get_current_user_id()) {
577 $result = $this->changeUserStatus(
578 $user->getPrincipal(), ($user->user_status ? 0 : 1)
579 );
580 }
581 }
582 }
583
584 return $result;
585 }
586
587 /**
588 * Change user status
589 *
590 * @param WP_User $user
591 * @param int $status
592 *
593 * @return boolean
594 *
595 * @access protected
596 * @version 6.0.0
597 */
598 protected function changeUserStatus(WP_User $user, $status)
599 {
600 global $wpdb;
601
602 $result = $wpdb->update(
603 $wpdb->users,
604 array('user_status' => $status),
605 array('ID' => $user->ID)
606 );
607
608 if ($result) {
609 $user->user_status = $status;
610 clean_user_cache($user);
611 }
612
613 return $result;
614 }
615
616 }
617
618 if (defined('AAM_KEY')) {
619 AAM_Service_SecureLogin::bootstrap();
620 }