PluginProbe ʕ •ᴥ•ʔ
Security Optimizer – The All-In-One Protection Plugin / 1.6.2
Security Optimizer – The All-In-One Protection Plugin v1.6.2
1.6.2 1.6.1 trunk 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0
sg-security / core / Sg_2fa / Sg_2fa.php
sg-security / core / Sg_2fa Last commit date
Sg_2fa.php 1 month ago
Sg_2fa.php
1046 lines
1 <?php
2 namespace SG_Security\Sg_2fa;
3
4 use SG_Security;
5 use SG_Security\Encryption_Service\Encryption_Service;
6 use SG_Security\Helper\User_Roles_Trait;
7 use PHPGangsta_GoogleAuthenticator;
8 use PragmaRX\Recovery\Recovery;
9 use \WP_Session_Tokens;
10 use SiteGround_Helper\Helper_Service;
11
12 /**
13 * Class that manages 2FA related services.
14 */
15 class Sg_2fa {
16
17 use User_Roles_Trait;
18
19 /**
20 * Local variables
21 *
22 * @var mixed
23 */
24 public $encryption_key_file;
25 public $google_authenticator;
26 public $recovery;
27 public $encryption;
28
29 /**
30 * The singleton instance.
31 *
32 * @since 1.1.1
33 *
34 * @var \Sg_2fa The singleton instance.
35 */
36 public static $instance;
37
38 /**
39 * User meta used by 2FA.
40 *
41 * @var array
42 */
43 public $user_2fa_meta = array(
44 // If the list is updated we need to update it in the uninstall file as well.
45 'configured',
46 'secret',
47 'qr',
48 'backup_codes',
49 );
50
51 /**
52 * The constructor.
53 *
54 * @since 1.0.0
55 */
56 public function __construct() {
57 // File path to the encryption key. If changed needs to be updated in uninstall as well.
58 $this->encryption_key_file = defined( 'SGS_ENCRYPTION_KEY_FILE_PATH' ) ? SGS_ENCRYPTION_KEY_FILE_PATH : WP_CONTENT_DIR . '/sgs_encrypt_key.php';
59 $this->google_authenticator = new PHPGangsta_GoogleAuthenticator();
60 $this->recovery = new Recovery();
61 $this->encryption = new Encryption_Service( $this->encryption_key_file );
62 }
63
64 /**
65 * Get the singleton instance.
66 *
67 * @since 1.1.1
68 *
69 * @return \Sg_2fa The singleton instance.
70 */
71 public static function get_instance() {
72 if ( null == self::$instance ) {
73 self::$instance = new self();
74 }
75 return self::$instance;
76 }
77
78 /**
79 * Generate QR code for specific user.
80 *
81 * @since 1.0.0
82 *
83 * @param int $user_id WordPress user ID.
84 *
85 * @return string The QR code URL.
86 */
87 public function generate_qr_code( $user_id ) {
88 // Get the user by ID.
89 $user = get_user_by( 'ID', $user_id );
90
91 // Build the title for the authenticator.
92 $title = get_home_url() . ' (' . $user->user_email . ')';
93
94 // Get the user secret code.
95 $secret = $this->get_user_secret( $user->ID ); // phpcs:ignore
96
97 // Return the URL.
98 return $this->google_authenticator->getQRCodeGoogleUrl( $title, $secret );
99 }
100
101 /**
102 * Verify the authenticaion code.
103 *
104 * @since 1.0.0
105 *
106 * @param string $code One time code from the authenticator app.
107 * @param int $user_id The user ID.
108 *
109 * @return bool True if the code is valid, false otherwise.
110 */
111 public function check_authentication_code( $code, $user_id ) {
112 // Get the user secret.
113 $secret = $this->get_user_secret( $user_id ); // phpcs:ignore
114
115 // Verify the code.
116 return $this->google_authenticator->verifyCode( $secret, $code, 2 );
117 }
118
119 /**
120 * Enable 2FA.
121 *
122 * @since 1.0.0
123 *
124 * @return bool True on success, false on failure.
125 */
126 public function enable_2fa() {
127 // Remove admin notice for file creation.
128 delete_option( 'sg_security_2fa_encryption_file_notice' );
129
130 // Get all users which needs to have 2FA enabled.
131 $users = get_users(
132 array(
133 'role__in' => $this->get_admin_user_roles(),
134 )
135 );
136
137 // Bail if there are no such users found.
138 if ( empty( $users ) ) {
139 return true;
140 }
141
142 foreach ( $users as $user ) {
143 // Get the user by the user id.
144 $user = get_userdata( $user->data->ID );
145
146 if ( empty( array_intersect( $this->get_admin_user_roles(), $user->roles ) ) ) {
147 continue;
148 }
149
150 $session_tokens = WP_Session_Tokens::get_instance( $user->data->ID );
151 $session_tokens->destroy_all();
152 }
153
154 return true;
155 }
156
157 /**
158 * Handle 2FA option change.
159 *
160 * @since 1.0.0
161 *
162 * @param mixed $new_value New option value.
163 * @param mixed $old_value Old option value.
164 */
165 public function handle_option_change( $new_value, $old_value ) {
166 if (
167 1 === intval( $new_value ) &&
168 false === $this->encryption->generate_encryption_file()
169 ) {
170 return $old_value;
171 }
172
173 if ( 1 == $new_value ) {
174 $this->enable_2fa();
175 }
176
177 return $new_value;
178 }
179
180 /**
181 * Generate the user secret.
182 *
183 * @since 1.0.0
184 *
185 * @param int $user_id WordPress user ID.
186 *
187 * @return mixed True on success, false on failure, user ID if the secret exists.
188 */
189 public function generate_user_secret( $user_id ) {
190 // Check if the user has secret code.
191 $secret = $this->get_user_secret( $user_id ); // phpcs:ignore
192
193 // Bail if the user already has a secret code.
194 if ( ! empty( $secret ) ) {
195 return $user_id;
196 }
197
198 // Add the user secret meta.
199 return update_user_meta( // phpcs:ignore
200 $user_id,
201 'sg_security_2fa_secret',
202 $this->encryption->sgs_encrypt( $this->google_authenticator->createSecret() ) // Generate and encrypt the secret code.
203 );
204 }
205
206 /**
207 * Generate the user backup codes.
208 *
209 * @since 1.1.0
210 *
211 * @param int $user_id WordPress user ID.
212 *
213 * @return mixed True on success, false on failure, user ID if the backup codes exists.
214 */
215 public function generate_user_backup_codes( $user_id ) {
216 // Check if the user has backup codes.
217 $backup_codes = get_user_meta( $user_id, 'sg_security_2fa_backup_codes', true ); // phpcs:ignore
218
219 // Bail if the user already has a backup codes.
220 if ( ! empty( $backup_codes ) ) {
221 return array();
222 }
223
224 // Generate the backup codes.
225 $generated_backup_codes = $this->recovery->numeric()->setCount( 8 )->setBlocks( 1 )->setChars( 8 )->toArray();
226
227 // Store the backup codes hashed.
228 $this->store_hashed_user_meta( $user_id, 'sg_security_2fa_backup_codes', $generated_backup_codes );
229
230 // Return the codes so we can show them to the user once.
231 return $generated_backup_codes;
232 }
233
234 /**
235 * Validate the backup codes 2Fa login.
236 *
237 * @since 1.1.0
238 *
239 * @param string $code The backup login code.
240 * @param int $user The user id.
241 *
242 * @return bool True if the code is correct, false on failure.
243 */
244 public function validate_backup_login( $code, $user ) {
245 $codes = get_user_meta( $user, 'sg_security_2fa_backup_codes', true ); // phpcs:ignore
246
247 // Bail if the user doesn't have backup codes.
248 if ( empty( $codes ) ) {
249 return false;
250 }
251
252 // Validate the backup code.
253 foreach ( $codes as $index => $hashed_code ) {
254 if ( wp_check_password( $code, $hashed_code ) ) {
255 // Remove the used key.
256 unset( $codes[ $index ] );
257
258 // Update user meta with the removed code data.
259 update_user_meta( $user, 'sg_security_2fa_backup_codes', $codes );
260
261 return true;
262 }
263 }
264
265 // Bail if the code doesn't exists in the user backup codes.
266 return false;
267 }
268
269 /**
270 * Display the two factor authentication forms.
271 *
272 * @since 1.0.0
273 *
274 * @param array $args Additional args.
275 */
276 public function load_form( $args ) {
277 // Bail if template is not provided.
278 if ( empty( $args['template'] ) ) {
279 return;
280 }
281
282 // Path to the form template.
283 $path = SG_Security\DIR . '/templates/' . $args['template'];
284
285 // Bail if there is no such file.
286 if ( ! file_exists( $path ) ) {
287 return;
288 }
289
290 $args = $this->get_args_for_template( $args );
291
292 // Check if the referer matches wp-login url.
293 if ( strtok( wp_get_raw_referer(), '?' ) === wp_login_url() ) {
294 $args['is_wp_login'] = true;
295 }
296
297 if ( ! empty( $this->get_2fa_nonce_cookie() ) ) {
298 $args['is_wp_login'] = true;
299 }
300
301 // Include the login header if the function doesn't exists.
302 if ( ! function_exists( 'login_header' ) ) {
303 include_once ABSPATH . 'wp-login.php';
304 }
305
306 // Include the template.php if the function doesn't exists.
307 if ( ! function_exists( 'submit_button' ) ) {
308 require_once ABSPATH . '/wp-admin/includes/template.php';
309 }
310
311 // JetPack SSO Hiding 2FA form.
312 if ( class_exists( 'Automattic\Jetpack\Connection\SSO' ) ) {
313 remove_filter( 'login_body_class', array( \Automattic\Jetpack\Connection\SSO::get_instance(), 'login_body_class' ) );
314 }
315
316 login_header();
317
318 // Include the template.
319 include_once $path;
320
321 login_footer();
322 exit;
323 }
324
325 /**
326 * Reset the 2FA for specific user ID.
327 *
328 * @since 1.1.1
329 *
330 * @param int $user_id WordPress user ID.
331 *
332 * @return array $response Responce to react app.
333 */
334 public function reset_user_2fa( $user_id ) {
335 // Bail if there is no such user.
336 if ( false === get_user_by( 'ID', $user_id ) ) {
337 return false;
338 }
339
340 // Delete the 2FA user meta and reset the 2FA configuration setting.
341 foreach ( $this->user_2fa_meta as $meta ) {
342 delete_user_meta( $user_id, 'sg_security_2fa_' . $meta ); // phpcs:ignore
343 }
344
345 return array(
346 'message' => __( 'User 2FA reset!', 'sg-security' ),
347 'result' => 1,
348 );
349 }
350
351 /**
352 * Default arguments passed to the form.
353 *
354 * @since 1.1.1
355 *
356 * @param array $args Аrguments passed.
357 *
358 * @return array Аrguments merged with the default ones.
359 */
360 public function get_args_for_template( $args ) {
361 return array_merge(
362 $args,
363 array(
364 'interim_login' => ( isset( $_REQUEST['interim-login'] ) ) ? filter_var( wp_unslash( $_REQUEST['interim-login'] ), FILTER_VALIDATE_BOOLEAN ) : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
365 'redirect_to' => isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : admin_url(), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
366 'rememberme' => ( ! empty( $_REQUEST['rememberme'] ) ) ? true : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended
367 'is_wp_login' => false,
368 'sg_security_2fa_do_not_challenge' => apply_filters( 'sg_security_2fa_do_not_challenge', true ),
369 )
370 );
371 }
372
373 /**
374 * Load the backup codes form.
375 *
376 * @since 1.1.0
377 */
378 public function load_backup_codes_form() {
379 // Get cookie data.
380 $cookie_data = $this->get_2fa_nonce_cookie();
381
382 // Bail if cookie data is empty.
383 if ( empty( $cookie_data ) ) {
384 return;
385 }
386
387 // Load the backup code login form.
388 $this->load_form(
389 array(
390 'template' => '2fa-login-backup-code.php',
391 'action' => esc_url( add_query_arg( 'action', 'sgs2fabc', wp_login_url() ) ),
392 'error' => '',
393 )
394 );
395 }
396
397 /**
398 * Set 30 days 2FA auth cookie.
399 *
400 * @since 1.2.6
401 *
402 * @param int $user_id WordPress user ID.
403 */
404 public function set_2fa_dnc_cookie( $user_id ) {
405 // Generate random token.
406 $token = bin2hex( random_bytes( 22 ) );
407
408 // Assign the token to the user.
409 update_user_meta( $user_id, 'sgs_2fa_dnc_token', $token );
410
411 $difference = '';
412 $domain = $_SERVER['SERVER_NAME'];
413 $domain_with_subfolder = get_home_url();
414 $protocol = isset( $_SERVER['HTTPS'] ) && ! empty( $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
415 $escaped_domain = preg_quote( $protocol . $domain, '/' );
416
417 if ( get_site_url() !== get_home_url() ) {
418 $domain = get_home_url();
419 $domain_with_subfolder = get_site_url();
420 $escaped_domain = preg_quote( $domain, '/' );
421 }
422
423 if ( preg_match( '/^' . $escaped_domain . '(.*)$/', $domain_with_subfolder, $matches ) ) {
424 $difference = $matches[1];
425 }
426
427 $domain = empty( COOKIE_DOMAIN ) ? $_SERVER['SERVER_NAME'] : COOKIE_DOMAIN;
428
429 // Set the 2FA auth cookie.
430 setcookie( 'sg_security_2fa_dnc_cookie', $user_id . '|' . $token, time() + 2592000, $difference . '/wp-login.php', $domain, true, true ); // phpcs:ignore
431 }
432
433 /**
434 * Check if there is a valid 2FA cookie.
435 *
436 * @since 1.1.1
437 *
438 * @param string $user_login The username.
439 * @param object $user WP_User object.
440 *
441 * @return bool True if there is a 2FA cookie, false if not.
442 */
443 public function check_2fa_cookie( $user_login, $user ) {
444 // 2FA user cookie name.
445 $sg_2fa_user_cookie = 'sg_security_2fa_dnc_cookie';
446
447 // Bail if the cookie doesn't exists.
448 if ( ! isset( $_COOKIE[ $sg_2fa_user_cookie ] ) ) {
449 return false;
450 }
451
452 // Bail if the 'do not challenge' filter is set to false.
453 if ( ! apply_filters( 'sg_security_2fa_do_not_challenge', true ) ) {
454 return false;
455 }
456
457 // Parse the cookie.
458 $cookie_data = explode( '|', $_COOKIE[ $sg_2fa_user_cookie ] );
459
460 if (
461 // If the 2FA is configured for the user.
462 1 == get_user_meta( $cookie_data[0], 'sg_security_2fa_configured', true ) && // phpcs:ignore
463 get_user_meta( $cookie_data[0], 'sgs_2fa_dnc_token', true ) === $cookie_data[1] // If there is already a cookie with that name and the name matches.
464 ) {
465 return true;
466 }
467
468 return false;
469 }
470
471 /**
472 * Show the backup codes form to the user if this is the initial 2fa setup.
473 *
474 * @since 1.1.1
475 *
476 * @param int $user_id WordPress user ID.
477 */
478 public function show_backup_codes( $user_id ) {
479 $this->load_form(
480 array(
481 'template' => 'backup-codes.php',
482 'backup_codes' => $this->generate_user_backup_codes( $user_id ),
483 'redirect_to' => ! empty( $_POST['redirect_to'] ) ? $_POST['redirect_to'] : get_admin_url(), // phpcs:ignore
484 )
485 );
486 }
487
488 /**
489 * Show QR code to the user if backup code is used.
490 *
491 * @since 1.1.1
492 *
493 * @param int $id WordPress user ID.
494 */
495 public function show_qr_backup_code_used() {
496 $this->load_form(
497 array(
498 'template' => 'backup-code-used.php',
499 'redirect_to' => ! empty( $_POST['redirect_to'] ) ? $_POST['redirect_to'] : get_admin_url(), // phpcs:ignore
500 )
501 );
502 }
503
504 /**
505 * Interim WordPress login.
506 *
507 * @since 1.1.1
508 */
509 public function interim_check() {
510 global $interim_login;
511 $interim_login = ( isset( $_REQUEST['interim-login'] ) ) ? filter_var( $_REQUEST['interim-login'], FILTER_VALIDATE_BOOLEAN ) : false; // phpcs:ignore
512
513 // Bail if $interim_login is false.
514 if ( false === $interim_login ) {
515 return;
516 }
517
518 $interim_login = 'success'; // WPCS: override ok.
519 login_header( '', '<p class="message">' . __( 'You have logged in successfully.', 'sg-security' ) . '</p>' );
520 ?>
521 </div>
522 <?php do_action( 'login_footer' ); ?>
523 </body></html>
524 <?php
525 exit;
526 }
527
528 /**
529 * Initialize the 2fa
530 *
531 * @since 1.0.0
532 *
533 * @param string $user_login The username.
534 * @param object $user WP_User object.
535 */
536 public function init_2fa( $user_login = null, $user = null ) {
537 // Bail, if the parameters are not provided correctly.
538 if ( false === $this->check_wp_login_params( $user_login, $user ) ) {
539 return;
540 }
541
542 // Bail if the user role does not allow 2FA setup.
543 if ( empty( array_intersect( $this->get_admin_user_roles(), $user->roles ) ) ) {
544 return;
545 }
546
547 // Bail if there is a valid 2FA cookie.
548 if ( true === $this->check_2fa_cookie( $user_login, $user ) ) {
549 return;
550 }
551
552 // Validate the encryption key.
553 if ( false === $this->encryption->get_encryption_key() ) {
554 // Disable the 2FA and show admin notice.
555 return $this->disable_2fa_show_notice();
556 }
557
558 // Remove the auth cookie.
559 wp_clear_auth_cookie();
560
561 $user_cookie_part = bin2hex( random_bytes( 18 ) );
562
563 $difference = '';
564 $domain = $_SERVER['SERVER_NAME'];
565 $domain_with_subfolder = get_home_url();
566 $protocol = isset( $_SERVER['HTTPS'] ) && ! empty( $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
567 $escaped_domain = preg_quote( $protocol . $domain, '/' );
568
569 if ( get_site_url() !== get_home_url() ) {
570 $domain = get_home_url();
571 $domain_with_subfolder = get_site_url();
572 $escaped_domain = preg_quote( $domain, '/' );
573 }
574
575 if ( preg_match( '/^' . $escaped_domain . '(.*)$/', $domain_with_subfolder, $matches ) ) {
576 $difference = $matches[1];
577 }
578
579 $domain = empty( COOKIE_DOMAIN ) ? $_SERVER['SERVER_NAME'] : COOKIE_DOMAIN;
580
581 $slug = '/wp-login.php';
582 // Check if the WPS Hide Login Plugin is active, if so, check it's option to see if there's a custom login URL set and use it.
583 if ( \class_exists( 'WPS\WPS_Hide_Login\Plugin' ) ) {
584 $slug = \get_option( 'whl_page', '' ) ?: '/login';
585 }
586
587 setcookie( 'sgs_2fa_login_nonce', $user->ID . '|' . $user_cookie_part, time() + DAY_IN_SECONDS, $difference . $slug, $domain, true, true );
588
589 update_user_meta( $user->ID, 'sgs_2fa_login_nonce', wp_hash( $user_cookie_part ) );
590
591 if ( 1 == get_user_meta( $user->ID, 'sg_security_2fa_configured', true ) ) { // phpcs:ignore
592 // Load the 2fa form.
593 $this->load_form(
594 array(
595 'action' => esc_url( add_query_arg( 'action', 'sgs2fa', wp_login_url() ) ),
596 'template' => '2fa-login.php',
597 'error' => '',
598 )
599 );
600 }
601
602 // Generate user secret code.
603 $this->generate_user_secret( $user->ID );
604
605 // Load the 2fa form.
606 $this->load_form(
607 array(
608 'action' => esc_url( add_query_arg( 'action', 'sgs2fa', wp_login_url() ) ),
609 'template' => '2fa-initial-setup-form.php',
610 'error' => '',
611 'qr' => $this->generate_qr_code( $user->ID ),
612 'secret' => $this->get_user_secret( $user->ID ),
613 )
614 );
615 }
616
617 /**
618 * Validate backup codes login.
619 *
620 * @since 1.1.0
621 */
622 public function validate_2fabc_login() {
623 // Get the cookie data.
624 $cookie_data = $this->get_2fa_nonce_cookie();
625
626 // Bail if cookie data is empty.
627 if ( empty( $cookie_data ) ) {
628 return;
629 }
630
631 $result = false;
632
633 // Check if the 2fa backup code is set, if not, don't try to apply it's value.
634 if ( isset( $_POST['sgc2fabackupcode'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
635 // Validate the backup code.
636 $result = $this->validate_backup_login(
637 wp_unslash( $_POST['sgc2fabackupcode'] ), // phpcs:ignore WordPress.Security.NonceVerification.Missing
638 wp_unslash( $cookie_data[0] )
639 ); // phpcs:ignore
640 }
641
642 // Check the result of the authtication.
643 if ( false === $result ) {
644 $this->load_form(
645 array(
646 'template' => '2fa-login-backup-code.php',
647 'action' => esc_url( add_query_arg( 'action', 'sgs2fabc', wp_login_url() ) ),
648 'error' => esc_html__( 'Invalid backup code!', 'sg-security' ),
649 )
650 );
651 }
652
653 // Login the user.
654 $this->login_user( $cookie_data[0] );
655
656 // Interim login.
657 $this->interim_check();
658
659 // Get the redirect url.
660 $redirect_url = ! empty( $_POST['redirect_to'] ) ? $_POST['redirect_to'] : get_admin_url(); // phpcs:ignore
661
662 if ( ! isset( $_POST['backup-code-used'] ) ) { // phpcs:ignore
663 // Retirect to the reset url.
664 wp_safe_redirect( esc_url_raw( wp_unslash( $redirect_url ) ) );
665 }
666
667 // Show QR code.
668 $this->show_qr_backup_code_used();
669 }
670
671 /**
672 * Validate 2FA login
673 *
674 * @since 1.1.0
675 */
676 public function validate_2fa_login() {
677 // Get the cookie data.
678 $cookie_data = $this->get_2fa_nonce_cookie();
679
680 // Bail if cookie data is empty.
681 if ( empty( $cookie_data ) ) {
682 return;
683 }
684
685 // Validate the encryption key.
686 if ( false === $this->encryption->get_encryption_key() ) {
687 // Disable the 2FA and show admin notice.
688 return $this->disable_2fa_show_notice();
689 }
690
691 $result = false;
692
693 // Check if the 2fa code is set, if not, don't try to apply it's value.
694 if ( isset( $_POST['sgc2facode'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
695 $result = $this->check_authentication_code( wp_unslash( $_POST['sgc2facode'] ), wp_unslash( $cookie_data[0] ) ); // phpcs:ignore
696 }
697
698 // Check the result of the authtication.
699 if ( false === $result ) {
700 // Arguments for 2fa login.
701 $args = array(
702 'template' => '2fa-login.php',
703 'error' => esc_html__( 'Invalid verification code!', 'sg-security' ),
704 'action' => esc_url( add_query_arg( 'action', 'sgs2fa', wp_login_url() ) ),
705 );
706
707 if ( 0 == get_user_meta( $cookie_data[0], 'sg_security_2fa_configured', true ) ) { // phpcs:ignore
708 // Arguments for initial 2fa setup.
709 $args = array_merge(
710 $args,
711 array(
712 'template' => '2fa-initial-setup-form.php',
713 'qr' => $this->generate_qr_code( $cookie_data[0] ),
714 'secret' => $this->get_user_secret( $cookie_data[0] ),
715 )
716 );
717 }
718
719 $this->load_form( $args ); // phpcs:ignore
720 }
721
722 // Login the user.
723 $this->login_user( $cookie_data[0] );
724
725 // Interim login.
726 $this->interim_check();
727
728 // Get the redirect url.
729 $redirect_url = ! empty( $_POST['redirect_to'] ) ? $_POST['redirect_to'] : get_admin_url(); // phpcs:ignore
730
731 // Show backup codes to the user in the initial 2FA setup.
732 if ( isset( $_POST['sgs-2fa-setup'] ) ) { // phpcs:ignore
733 $this->show_backup_codes( $cookie_data[0] );
734 }
735
736 // Retirect to the reset url.
737 wp_safe_redirect( esc_url_raw( wp_unslash( $redirect_url ) ) );
738 }
739
740 /**
741 * Login the user.
742 *
743 * @since 1.2.5
744 *
745 * @param int $user_id The user id.
746 */
747 private function login_user( $user_id ) {
748 // Set the auth cookie.
749 wp_set_auth_cookie( wp_unslash( $user_id ), intval( wp_unslash( $_POST['rememberme'] ) ) ); // phpcs:ignore
750
751 // Delete the nonce meta.
752 delete_user_meta( $user_id, 'sgs_2fa_login_nonce' );
753 $difference = '';
754 $domain = $_SERVER['SERVER_NAME'];
755 $domain_with_subfolder = get_home_url();
756 $protocol = isset( $_SERVER['HTTPS'] ) && ! empty( $_SERVER['HTTPS'] ) ? 'https://' : 'http://';
757 $escaped_domain = preg_quote( $protocol . $domain, '/' );
758
759 if ( get_site_url() !== get_home_url() ) {
760 $domain = get_home_url();
761 $domain_with_subfolder = get_site_url();
762 $escaped_domain = preg_quote( $domain, '/' );
763 }
764
765 if ( preg_match( '/^' . $escaped_domain . '(.*)$/', $domain_with_subfolder, $matches ) ) {
766 $difference = $matches[1];
767 }
768
769 $domain = empty( COOKIE_DOMAIN ) ? $_SERVER['SERVER_NAME'] : COOKIE_DOMAIN;
770
771 // Delete the nonce cookie.
772 setcookie( 'sgs_2fa_login_nonce', '', -1, $difference . '/wp-login.php', $domain, true, true );
773
774 // Set 30 days 2FA auth cookie.
775 if ( isset( $_POST['do_not_challenge'] ) ) { // phpcs:ignore
776 $this->set_2fa_dnc_cookie( $user_id );
777 }
778
779 // Update the user meta if this is the inital 2FA setup.
780 if ( ! isset( $_POST['sgs-2fa-setup'] ) ) { // phpcs:ignore
781 return;
782 }
783
784 // Set a flag, that the user has configured the 2fa.
785 update_user_meta( $user_id, 'sg_security_2fa_configured', 1 ); // phpcs:ignore
786
787 // Invalidate 2FA cookie.
788 setcookie( 'sg_security_2fa_dnc_cookie', '', -1 ); // phpcs:ignore
789 }
790
791 /**
792 * Get the 2fa nonce cookie
793 *
794 * @since 1.2.6
795 *
796 * @return mixed Cookie data if the cookie exists, null otherwise.
797 */
798 public function get_2fa_nonce_cookie() {
799 // Bail if the cookie doesn't exists.
800 if ( empty( $_COOKIE['sgs_2fa_login_nonce'] ) ) {
801 return;
802 }
803
804 // Parse the cookie.
805 $cookie_data = explode( '|', $_COOKIE['sgs_2fa_login_nonce'] );
806 // Get the user nonce meta.
807 $meta_nonce = get_user_meta( $cookie_data[0], 'sgs_2fa_login_nonce', true );
808
809 if ( empty( $meta_nonce ) || empty( $cookie_data[0] ) ) {
810 return;
811 }
812
813 // Bail if the nonce is invalid.
814 if ( ! hash_equals( $meta_nonce, wp_hash( $cookie_data[1] ) ) ) {
815 return;
816 }
817
818 // Return the cookie data.
819 return $cookie_data;
820 }
821
822 /**
823 * Check for all users with 2fa setup.
824 *
825 * @since 1.1.1
826 *
827 * @return array The array containining the users using 2FA.
828 */
829 public function check_for_users_using_2fa() {
830 // Get all users with 2FA configured.
831 $users = get_users(
832 array(
833 'role__in' => $this->get_admin_user_roles(),
834 'orderby' => 'user_login',
835 'order' => 'ASC',
836 'fields' => array(
837 'ID',
838 'user_login',
839 ),
840 'meta_query' => array(
841 array(
842 'key' => 'sg_security_2fa_configured',
843 'value' => '1',
844 'compare' => '=',
845 ),
846 ),
847 )
848 );
849
850 return $users;
851 }
852
853 /**
854 * Stores a hashed user meta.
855 *
856 * @since 1.3.2
857 *
858 * @param int $user_id The user ID
859 * @param string $meta The user meta
860 * @param array $data The data to be hashed
861 *
862 * @return int|bool Meta ID if the key didn't exist, true on successful update, false on failure.
863 */
864 public function store_hashed_user_meta( $user_id, $meta, $data = array() ) {
865 // Bail if data is not an array.
866 if ( ! is_array( $data ) ) {
867 return false;
868 }
869
870 // Prepare the array.
871 $hashed_data = array();
872
873 // Hash the data.
874 foreach ( $data as $key => $value ) {
875 $hashed_value = wp_hash_password( $value );
876 $hashed_data[] = $hashed_value;
877 }
878
879 // Add the user hashed meta.
880 return update_user_meta( $user_id, $meta, $hashed_data );
881 }
882
883 /**
884 * Gets the user secret.
885 *
886 * @since 1.3.6
887 *
888 * @param int $user_id The user identifier
889 */
890 public function get_user_secret( $user_id ) {
891 // Get the encrypted secret code of the user.
892 $user_secret = get_user_meta( $user_id, 'sg_security_2fa_secret', true );
893
894 // Bail if the user ID or meta value does not exist.
895 if ( empty( $user_secret ) ) {
896 return;
897 }
898
899 // Decrypt and return the secret code.
900 return $this->encryption->sgs_decrypt( $user_secret );
901 }
902
903 /**
904 * Reset 2FA for all users.
905 *
906 * @since 1.3.6
907 */
908 public function reset_all_users_2fa() {
909 // Delete the 2FA user meta and reset the 2FA configuration setting.
910 foreach ( $this->user_2fa_meta as $meta ) {
911 delete_metadata( 'user', 0, 'sg_security_2fa_' . $meta, '', true );
912 }
913 }
914
915 /**
916 * Disables the 2FA and shows admin notice.
917 */
918 public function disable_2fa_show_notice() {
919 // Disable 2FA.
920 update_option( 'sg_security_sg2fa', 0 ); // phpcs:ignore
921 // Reset all users 2FA setup.
922 $this->reset_all_users_2fa();
923 // Show admin notice for file creation failure.
924 update_option( 'sg_security_2fa_encryption_file_notice', 1 ); // phpcs:ignore
925 }
926
927 /**
928 * Displays an admin notice that we were not able to create encryption file.
929 *
930 * @since 1.3.6
931 */
932 public function show_notices() {
933 // Bail if there is no need of a notice.
934 if ( empty( get_option( 'sg_security_2fa_encryption_file_notice', false ) ) ) {
935 return;
936 }
937
938 printf(
939 '<div class="notice notice-error sg sg-section__content" style="position: relative; margin-top: 1em; display:block!important;"><p>%1$s</p><button type="button" class="notice-dismiss dismiss-sg-security-notice" data-link="%2$s"><span class="screen-reader-text">Dismiss this notice.</span></button></div>',
940 __( 'SG Security: We were not able to create encryption file used by 2FA, so the Two Factor Authentication service was disabled. Please check your website files and folders permissions or contact your hosting provider for assistance.', 'sg-security' ), // phpcs:ignore
941 wp_nonce_url( admin_url( 'admin-ajax.php?action=dismiss_sgs_2fa_notice&notice=2fa_encryption_file_notice' ), 'sg-security-2fa-file-notice' ) // phpcs:ignore
942 );
943 }
944
945 /**
946 * Hide notices.
947 *
948 * @since 1.3.6
949 */
950 public function hide_notice() {
951 if ( empty( $_GET['notice'] ) || ! check_ajax_referer( 'sg-security-2fa-file-notice', 'nonce', false ) ) {
952 return;
953 }
954
955 if ( '2fa_encryption_file_notice' !== sanitize_text_field( $_GET['notice'] ) ) {
956 return;
957 }
958
959 if ( ! current_user_can( 'activate_plugins' ) ) {
960 return;
961 }
962
963 update_option( 'sg_security_2fa_encryption_file_notice', 0 );
964
965 wp_send_json_success();
966 }
967
968 /**
969 * Check if encryption file was migrated over and move it back to wp-content directory.
970 *
971 * @since 1.3.6
972 */
973 public function move_encryption_file() {
974 // Setup the WP Filesystem.
975 $wp_filesystem = Helper_Service::setup_wp_filesystem();
976
977 // Bail if the encryption file already exists.
978 if ( $wp_filesystem->is_file( $this->encryption_key_file ) ) {
979 return;
980 }
981
982 // Check if the file was migrated over with SG Migrator.
983 if ( ! $wp_filesystem->is_file( WP_PLUGIN_DIR . '/sg-security/sgs_encrypt_key.php' ) ) {
984 return;
985 }
986
987 // Move the file back to the original location.
988 $wp_filesystem->move( WP_PLUGIN_DIR . '/sg-security/sgs_encrypt_key.php', $this->encryption_key_file );
989 }
990
991 /**
992 * Checks if the correct 'wp_login' parameters are provided.
993 *
994 * @param string $user_login The username.
995 * @param object $user WP_User object.
996 *
997 * @return bool False if incorrect parameters are provided, true if they are correct.
998 */
999 public function check_wp_login_params( &$user_login, &$user ) {
1000 // If we have only WP_User object and no username, recover the username and continue the login.
1001 if ( empty( $user_login ) && $user instanceof \WP_User ) {
1002 // If its admin user trying to log in with broken parameters, bail.
1003 if ( ! empty( array_intersect( $this->get_admin_user_roles(), $user->roles ) ) ) {
1004 wp_clear_auth_cookie();
1005 wp_set_current_user( 0 );
1006 return false;
1007 }
1008
1009 // Recover the username.
1010 $user_login = $user->user_login;
1011 }
1012
1013 $maybe_user = null;
1014
1015 // If we have username but no WP_User object, recover the object.
1016 if ( ! ( $user instanceof \WP_User ) && ! empty( $user_login ) ) {
1017 $maybe_user = get_user_by( 'login', $user_login );
1018
1019 // Guard against, runtime created user, that is not yet in the DB.
1020 if ( ! $maybe_user && is_user_logged_in() ) {
1021 $maybe_user = wp_get_current_user();
1022 }
1023
1024 // If its admin user trying to log in with broken parameters, bail.
1025 if ( $maybe_user instanceof \WP_User ) {
1026 if ( ! empty( array_intersect( $this->get_admin_user_roles(), $maybe_user->roles ) ) ) {
1027 wp_clear_auth_cookie();
1028 wp_set_current_user( 0 );
1029 return false;
1030 }
1031 }
1032
1033 // If the user is not admin, but was missing, assign it.
1034 $user = $maybe_user;
1035 }
1036
1037 // Bail, if still broken at this point.
1038 if ( empty( $user_login ) || ! ( $user instanceof \WP_User ) ) {
1039 return false;
1040 }
1041
1042 // All checks are passed.
1043 return true;
1044 }
1045 }
1046