PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / class-two-fa.php

class-two-fa.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.0, at src/component/class-two-fa.php

890 lines 26.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles all operations related to two-factor authentication (2FA) for users.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_User;
11 use WP_Error;
12 use Countable;
13 use WP_User_Query;
14 use SodiumException;
15 use WP_Defender\Traits\IP;
16 use Calotes\Base\Component;
17 use WP_Defender\Traits\User;
18 use Calotes\Helper\Array_Cache;
19 use WP_Defender\Controller\Two_Factor;
20 use WordfenceLS\Controller_WordfenceLS;
21 use WP_Defender\Model\Setting\Login_Lockout;
22 use WP_Defender\Component\Two_Factor\Providers\Totp;
23 use WP_Defender\Model\Setting\Two_Fa as Two_Fa_Model;
24 use WP_Defender\Component\Two_Factor\Providers\Webauthn;
25 use WP_Defender\Component\Two_Factor\Providers\Backup_Codes;
26 use WP_Defender\Component\Two_Factor\Providers\Fallback_Email;
27
28 /**
29 * Handles all operations related to two-factor authentication (2FA) for users.
30 */
31 class Two_Fa extends Component {
32
33 use User;
34 use IP;
35
36 /**
37 * The user meta key for the default provider.
38 *
39 * @var string
40 */
41 public const DEFAULT_PROVIDER_USER_KEY = 'wd_2fa_default_provider';
42
43 /**
44 * The user meta key for enabled providers.
45 *
46 * @var string
47 */
48 public const ENABLED_PROVIDERS_USER_KEY = 'wd_2fa_enabled_providers';
49
50 /**
51 * Default limit for failed 2FA attempts.
52 *
53 * @var int
54 */
55 public const ATTEMPT_LIMIT = 5;
56
57 /**
58 * Default time limit for 2FA attempt window in seconds.
59 *
60 * @var int
61 */
62 public const TIME_LIMIT = 30 * MINUTE_IN_SECONDS;
63
64 /**
65 * User meta key for 2FA tokens.
66 *
67 * @var string
68 */
69 public const TOKEN_USER_KEY = 'defender_two_fa_token';
70
71 /**
72 * Get the limit of failed attempts.
73 *
74 * @return int
75 * @since 3.3.0
76 */
77 public function get_attempt_limit(): int {
78 $limit = apply_filters( 'wd_2fa_attempt_limit', self::ATTEMPT_LIMIT );
79 return is_int( $limit ) ? $limit : (int) $limit;
80 }
81
82 /**
83 * Get the limit of time.
84 *
85 * @return int
86 */
87 public function get_time_limit() {
88 $limit = apply_filters( 'wd_2fa_time_limit', self::TIME_LIMIT );
89 return is_int( $limit ) ? $limit : (int) $limit;
90 }
91
92 /**
93 * Enable OTP for a user.
94 *
95 * @param int $user_id The ID of the user.
96 *
97 * @return void
98 */
99 public function enable_otp( int $user_id ): void {
100 update_user_meta( $user_id, Totp::TOTP_AUTH_KEY, 1 );
101 update_user_meta( $user_id, Totp::TOTP_FORCE_KEY, 0 );
102 }
103
104 /**
105 * Gradually move on to using the method is_enabled_otp_for_user(). Used only by the Forminator plugin.
106 *
107 * @param int $user_id User ID to check.
108 *
109 * @return mixed
110 */
111 public function is_user_enabled_otp( int $user_id ) {
112 return get_user_meta( $user_id, Totp::TOTP_AUTH_KEY, true );
113 }
114
115 /**
116 * Checks if forced authentication is enabled for specific user roles.
117 *
118 * @param int $user_id The user ID.
119 * @param array $roles The user roles to check.
120 *
121 * @return bool True if forced authentication is enabled, false otherwise.
122 */
123 public function is_force_auth_enable_for( int $user_id, array $roles ): bool {
124 $user = get_user_by( 'id', $user_id );
125 if ( ! is_object( $user ) ) {
126 return false;
127 }
128
129 $check = array_intersect( $this->get_roles( $user ), $roles );
130
131 return count( $check ) > 0;
132 }
133
134 /**
135 * Count the total of users, who enables any 2FA method.
136 *
137 * @return int
138 */
139 public function count_users_with_enabled_2fa(): int {
140 $slugs = array_keys( $this->get_providers() );
141 $query = new WP_User_Query(
142 array(
143 // Look over the network.
144 'blog_id' => 0,
145 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
146 array(
147 'key' => self::DEFAULT_PROVIDER_USER_KEY,
148 'value' => $slugs,
149 'compare' => 'IN',
150 ),
151 ),
152 )
153 );
154
155 return $query->get_total();
156 }
157
158 /**
159 * Checks if Jetpack SSO is in conflict with 2FA settings.
160 *
161 * @return bool|int True if there is a conflict, false otherwise, or 0 if undetermined.
162 */
163 public function is_jetpack_sso() {
164 $settings = new Two_Fa_Model();
165 if ( is_plugin_active_for_network( 'jetpack/jetpack.php' ) ) {
166 // Loop through all sites.
167 $is_conflict = $settings->is_conflict( 'jetpack/jetpack.php' );
168 if ( 0 === $is_conflict ) {
169 $blog_ids = get_sites( array( 'fields' => 'ids' ) );
170 if ( is_array( $blog_ids ) ) {
171 foreach ( $blog_ids as $id ) {
172 $options = get_blog_option( $id, 'jetpack_active_modules', array() );
173 if ( array_search( 'sso', $options, true ) ) {
174 $settings->mark_as_conflict( 'jetpack/jetpack.php' );
175
176 return true;
177 }
178 }
179 }
180 } else {
181 // Get the data from cache.
182 return $is_conflict;
183 }
184 } elseif ( is_plugin_active( 'jetpack/jetpack.php' ) ) {
185 $is_conflict = $settings->is_conflict( 'jetpack/jetpack.php' );
186 if ( 0 === $is_conflict ) {
187 $options = get_option( 'jetpack_active_modules', array() );
188 if ( array_search( 'sso', $options, true ) ) {
189 $settings->mark_as_conflict( 'jetpack/jetpack.php' );
190
191 return true;
192 }
193 } else {
194 return $is_conflict;
195 }
196 }
197
198 return false;
199 }
200
201 /**
202 * Checks if Theme My Login plugin is active and marks it as a conflict.
203 *
204 * @return bool True if Theme My Login is active, false otherwise.
205 */
206 public function is_tml(): bool {
207 if (
208 is_plugin_active( 'theme-my-login/theme-my-login.php' )
209 || is_plugin_active_for_network( 'theme-my-login/theme-my-login.php' )
210 ) {
211 $settings = new Two_Fa_Model();
212 $settings->mark_as_conflict( 'theme-my-login/theme-my-login.php' );
213
214 return true;
215 }
216
217 return false;
218 }
219
220 /**
221 * Retrieves the custom graphic URL for 2FA, with fallback to a default image if necessary.
222 *
223 * @param string $url Optional. The custom URL to validate.
224 *
225 * @return string The URL to the graphic.
226 */
227 public function get_custom_graphic_url( string $url = '' ): string {
228 if ( '' === $url ) {
229 // Nothing here, surely it will cause broken, fall back to default.
230 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
231 } else {
232 // Image should be under wp-content/.., so we catch that part.
233 if ( preg_match( '/(\/wp-content\/.+)/', $url, $matches ) ) {
234 $rel_path = $matches[1];
235 $rel_path = ltrim( $rel_path, '/' );
236 $abs_path = ABSPATH . $rel_path;
237 if ( ! file_exists( $abs_path ) ) {
238 // Fallback.
239 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
240 } else {
241 // Should replace with our site url.
242 return get_site_url( null, $rel_path );
243 }
244 }
245
246 return defender_asset_url( '/assets/img/2factor-disabled.svg' );
247 }
248 }
249
250 /**
251 * Checks if 2FA is enabled for the current role of the specified user.
252 *
253 * @param WP_User $user The user to check.
254 *
255 * @return bool True if 2FA is enabled for the user's role, false otherwise.
256 */
257 public function is_enable_for_current_role( WP_User $user ): bool {
258 if ( 0 === ( is_array( $user->roles ) || $user->roles instanceof Countable ? count( $user->roles ) : 0 ) ) {
259 return true;
260 }
261
262 $settings = new Two_Fa_Model();
263 if ( ! is_multisite() ) {
264 $allowed_for_this_role = array_intersect( $settings->user_roles, $user->roles );
265 if ( ! is_array( $allowed_for_this_role ) ) {
266 $allowed_for_this_role = array();
267 }
268
269 return count( $allowed_for_this_role ) > 0;
270 } else {
271 $blogs = get_blogs_of_user( $user->ID );
272 $user_roles = array();
273 foreach ( $blogs as $blog ) {
274 // Get user roles for this blog.
275 $u = new WP_User( $user->ID, '', $blog->userblog_id );
276 $user_roles = array_merge( $u->roles, $user_roles );
277 }
278 $allowed_for_this_role = array_intersect( $settings->user_roles, $user_roles );
279
280 return count( $allowed_for_this_role ) > 0;
281 }
282 }
283
284 /**
285 * Checks if OTP is enabled for a specific user.
286 *
287 * @param int|WP_User $user_value The user ID or WP_User object.
288 *
289 * @return bool True if OTP is enabled, false otherwise.
290 */
291 public function is_enabled_otp_for_user( $user_value ): bool {
292 if ( is_numeric( $user_value ) ) {
293 $user = get_user_by( 'id', $user_value );
294 } elseif ( $user_value instanceof WP_User ) {
295 $user = $user_value;
296 } else {
297 return false;
298 }
299
300 if ( ! $this->is_enable_for_current_role( $user ) ) {
301 return false;
302 }
303
304 return (bool) get_user_meta( $user->ID, Totp::TOTP_AUTH_KEY, true );
305 }
306
307 /**
308 * Checks if the specified user roles intersect with the plugin's user roles.
309 *
310 * @param array $current_user_roles The current user roles.
311 * @param array $plugin_user_roles The plugin's user roles.
312 *
313 * @return bool True if there is an intersection, false otherwise.
314 */
315 public function is_intersected_arrays( array $current_user_roles, array $plugin_user_roles ): bool {
316 return array() !== array_intersect( $current_user_roles, $plugin_user_roles );
317 }
318
319 /**
320 * Retrieves all 2FA providers.
321 *
322 * @return array An array of 2FA providers.
323 */
324 public function get_providers(): array {
325 $providers = Array_Cache::get( 'providers', 'two_fa' );
326 if ( ! is_array( $providers ) ) {
327 $classes = array(
328 Totp::class,
329 Backup_Codes::class,
330 Fallback_Email::class,
331 Webauthn::class,
332 );
333 /**
334 * Filter the supplied providers.
335 *
336 * @param array $classes
337 *
338 * @since 2.8.0
339 */
340 $classes = apply_filters( 'wd_2fa_providers', $classes );
341 foreach ( $classes as $class ) {
342 $providers[ $class::$slug ] = new $class();
343 }
344 Array_Cache::set( 'providers', $providers, 'two_fa' );
345 }
346
347 return $providers;
348 }
349
350 /**
351 * Get all 2FA Auth providers that are enabled for the specified|current user.
352 *
353 * @param WP_User|null $user The user to check.
354 *
355 * @return array
356 */
357 public function get_enabled_providers_for_user( $user = null ): array {
358 if ( ! ( $user instanceof WP_User ) ) {
359 $user = wp_get_current_user();
360 }
361
362 $providers = $this->get_providers();
363 $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, true );
364 if ( ! is_array( $enabled_providers ) || array() === $enabled_providers ) {
365 $enabled_providers = array();
366 }
367 $enabled_providers = array_intersect( $enabled_providers, array_keys( $providers ) );
368
369 /**
370 * Filter the enabled 2FA providers for this user.
371 *
372 * @param array $enabled_providers The enabled providers.
373 * @param int $user_id The user ID.
374 *
375 * @since 2.8.0
376 */
377 return apply_filters( 'wd_2fa_enabled_providers_for_user', $enabled_providers, $user->ID );
378 }
379
380 /**
381 * Retrieves all available 2FA providers for the specified or current user.
382 *
383 * @param WP_User|null $user The user to check. If null, the current user is used.
384 *
385 * @return array An array of available 2FA providers.
386 */
387 public function get_available_providers_for_user( $user = null ): array {
388 if ( ! ( $user instanceof WP_User ) ) {
389 $user = wp_get_current_user();
390 }
391
392 $providers = $this->get_providers();
393 $enabled_providers = $this->get_enabled_providers_for_user( $user );
394 $configured_providers = array();
395
396 foreach ( $providers as $slug => $provider ) {
397 if ( in_array( $slug, $enabled_providers, true ) && $provider->is_available_for_user( $user ) ) {
398 $configured_providers[ $slug ] = $provider;
399 }
400 }
401
402 return $configured_providers;
403 }
404
405 /**
406 * Gets the 2FA provider's slug for the specified or current user.
407 *
408 * @param int $user_id Optional. User ID. Default is 'null'.
409 *
410 * @return string|null
411 */
412 public function get_default_provider_slug_for_user( $user_id = null ) {
413 if ( is_null( $user_id ) || ! is_numeric( $user_id ) ) {
414 $user_id = get_current_user_id();
415 }
416
417 $available_providers = $this->get_available_providers_for_user( get_userdata( $user_id ) );
418 // If there's only one available provider, force that to be the primary.
419 if ( array() === $available_providers ) {
420 return null;
421 } elseif ( 1 === count( $available_providers ) ) {
422 $provider_slug = key( $available_providers );
423 } else {
424 $provider_slug = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
425
426 // If the provider specified isn't enabled, just grab the first one that is.
427 if ( ! isset( $available_providers[ $provider_slug ] ) ) {
428 $provider_slug = key( $available_providers );
429 }
430 }
431
432 /**
433 * Filter the 2FA provider slug used for this user.
434 *
435 * @param string $provider_slug The provider slug currently being used.
436 * @param int $user_id The user ID.
437 */
438 return apply_filters( 'wd_2fa_default_provider_for_user', $provider_slug, $user_id );
439 }
440
441 /**
442 * Checks if a specific 2FA provider is enabled for a user by its slug.
443 *
444 * @param WP_User $user The user to check.
445 * @param string $slug The provider slug.
446 *
447 * @return bool True if the provider is enabled, false otherwise.
448 */
449 public function is_checked_enabled_provider_by_slug( WP_User $user, string $slug ): bool {
450 $enabled_providers = $this->get_enabled_providers_for_user( $user );
451
452 return in_array( $slug, $enabled_providers, true );
453 }
454
455 /**
456 * Send emergency email to users.
457 *
458 * @param string $login_token This will be generated randomly on frontend each time user refresh, an internal
459 * OTP.
460 * @param int $user_id User ID to send email to.
461 *
462 * @return boolean|WP_Error
463 */
464 public function send_otp_to_email( string $login_token, int $user_id ) {
465 if ( $user_id <= 0 ) {
466 return new WP_Error( Error_Code::INVALID, esc_html__( 'The user is invalid.', 'defender-security' ) );
467 }
468 $settings = new Two_Fa_Model();
469 $hashed_token = get_user_meta( $user_id, self::TOKEN_USER_KEY, true );
470 if ( ! Crypt::compare_lines( $hashed_token, wp_hash( $user_id . $login_token ) ) ) {
471 return new WP_Error( Error_Code::INVALID, esc_html__( 'Your token is invalid.', 'defender-security' ) );
472 }
473
474 $code = wp_generate_password( 20, false );
475 update_user_meta(
476 $user_id,
477 Fallback_Email::FALLBACK_BACKUP_CODE_KEY,
478 array(
479 'code' => wp_hash( $code ),
480 'time' => time(),
481 )
482 );
483 $user = get_user_by( 'id', $user_id );
484 $params = array(
485 'display_name' => $user->display_name,
486 'passcode' => $code,
487 );
488 $two_fa = wd_di()->get( Two_Factor::class );
489 $body = $two_fa->render_partial(
490 'email/2fa-lost-phone',
491 array(
492 'body' => $settings->email_body,
493 ),
494 false
495 );
496
497 foreach ( $params as $key => $val ) {
498 if ( 'passcode' === $key ) {
499 $body = str_replace( "{{{$key}}}", '<span class="defender-otp">' . $val . '</span>', $body );
500 } else {
501 $body = str_replace( "{{{$key}}}", $val, $body );
502 }
503 }
504 // Main email template.
505 $body = $two_fa->render_partial(
506 'email/index',
507 array(
508 'title' => esc_html__( 'Two-Factor Authentication', 'defender-security' ),
509 'content_body' => $body,
510 // An empty value because 2FA-email is sent after a manual click from the user.
511 'unsubscribe_link' => '',
512 ),
513 false
514 );
515 // Since v5.2.0.
516 $from_email = defender_noreply_email( 'wd_two_fa_totp_noreply_email' );
517 $headers = array( 'Content-Type: text/html; charset=UTF-8' );
518 $headers[] = sprintf( 'From: %s <%s>', $settings->email_sender, $from_email );
519
520 return wp_mail( Fallback_Email::get_backup_email( $user->ID ), $settings->email_subject, $body, $headers );
521 }
522
523 /**
524 * Retrieves a 2FA provider by its slug.
525 *
526 * @param string $slug The provider slug.
527 *
528 * @return object|WP_Error The provider object if found, WP_Error otherwise.
529 */
530 public function get_provider_by_slug( $slug ) {
531 foreach ( $this->get_providers() as $key => $provider ) {
532 if ( $slug === $key ) {
533 return $provider;
534 }
535 }
536
537 return new WP_Error( 'opt_fail', esc_html__( 'ERROR: Cheatin&#8217; uh?', 'defender-security' ) );
538 }
539
540 /**
541 * Remove enabled provider for the specified|current user.
542 *
543 * @param string $provider A provider which needs to be removed.
544 * @param WP_User|null $user The current user to be removed.
545 *
546 * @return void
547 * @since 3.0.0
548 */
549 public function remove_enabled_provider_for_user( string $provider, $user = null ): void {
550 if ( ! ( $user instanceof WP_User ) ) {
551 $user = wp_get_current_user();
552 }
553
554 $enabled_providers = get_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, true );
555 if ( ! is_array( $enabled_providers ) || array() === $enabled_providers ) {
556 return;
557 }
558
559 $pos = array_search( $provider, $enabled_providers, true );
560 if ( false !== $pos ) {
561 unset( $enabled_providers[ $pos ] );
562 update_user_meta( $user->ID, self::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
563
564 $default_provider = get_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY, true );
565 if ( $provider === $default_provider ) {
566 delete_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY );
567 }
568 }
569 }
570
571 /**
572 * Verifies the attempt for two-factor authentication.
573 *
574 * @param int $user_id The ID of the user.
575 * @param string $slug The slug of the two-factor authentication method.
576 *
577 * @return string The lockout message, if the attempt is invalid.
578 * @since 3.3.0
579 */
580 public function verify_attempt( int $user_id, string $slug ): string {
581 $lockout_message = '';
582 $login_settings = wd_di()->get( Login_Lockout::class );
583
584 $firewall_component = wd_di()->get( Firewall::class );
585
586 $skip_priority_lockout_checks = true;
587 foreach ( $this->get_user_ip() as $ip ) {
588 if ( ! $firewall_component->skip_priority_lockout_checks( $ip ) ) {
589 $skip_priority_lockout_checks = false;
590 break;
591 }
592 }
593
594 /**
595 * Required rules:
596 * hook returns true (since v3.3.0),
597 * if Login Protection is enabled,
598 * the current IP is not in priority points: no allowlisted, no staff and the country is not from allowlisted.
599 */
600 if (
601 apply_filters( 'wd_2fa_enable_attempts', true )
602 && $login_settings->enabled
603 && ! $skip_priority_lockout_checks
604 ) {
605 $line = get_user_meta( $user_id, 'wd_2fa_attempt_' . $slug, true );
606 // Fresh start or there's a record.
607 if ( in_array( $line, array( false, '', array() ), true ) ) {
608 $count = $this->get_attempt_limit();
609 $start_time = time();
610 } else {
611 [ $count, $start_time ] = explode( '::', $line );
612 }
613 $end_time = time();
614 $time_limit = $this->get_time_limit();
615 // If the time difference between attempts is greater than the limit, clear the attempt counter.
616 if ( $end_time - $start_time >= $time_limit ) {
617 delete_user_meta( $user_id, 'wd_2fa_attempt_' . $slug );
618 $count = $this->get_attempt_limit();
619 $start_time = $end_time;
620 }
621
622 $count = (int) $count - 1;
623 if ( 1 > $count ) {
624 $message = sprintf(
625 /* translators: %s: 2FA method slug. */
626 esc_html__( 'Lockout occurred: Too many failed 2fa attempts for %s method.', 'defender-security' ),
627 $slug
628 );
629 // since 3.3.0.
630 do_action( 'wd_2fa_lockout', $user_id, $message, $time_limit );
631
632 delete_user_meta( $user_id, 'wd_2fa_attempt_' . $slug );
633
634 wp_safe_redirect( home_url() );
635 exit;
636 }
637 // Save number of attempts.
638 update_user_meta( $user_id, 'wd_2fa_attempt_' . $slug, $count . '::' . $start_time );
639 $lockout_message = esc_html__( 'INVALID CODE: ', 'defender-security' );
640 $lockout_message .= '<span class="two-factor-attempt">' . esc_html__(
641 'The two-factor authentication code you entered is incorrect or has expired. Please try again.',
642 'defender-security'
643 );
644 $lockout_message .= '<br/><br/>';
645 $lockout_message .= sprintf(
646 /* translators: %d: Count. */
647 _n( 'You have %d login attempt remaining.', 'You have %d login attempts remaining.', $count, 'defender-security' ),
648 $count
649 );
650 $lockout_message .= '</span>';
651 }
652
653 return $lockout_message;
654 }
655
656 /**
657 * Encrypts user data with a new key.
658 *
659 * @param int $user_id The ID of the user.
660 * @param string $plaintext The plaintext data to be encrypted.
661 * @param string $state The state of the data before encryption.
662 *
663 * @return bool True if the data was successfully encrypted, false otherwise.
664 * @throws SodiumException On failure.
665 */
666 protected function reencrypt_data( $user_id, $plaintext, $state = 'plaintext' ) {
667 $new_key = Crypt::get_encrypted_data( $plaintext );
668 if ( ! is_wp_error( $new_key ) ) {
669 // If everything is successful, remove an old key.
670 delete_user_meta( $user_id, TOTP::TOTP_SECRET_KEY );
671 // Update the value with a new key.
672 update_user_meta( $user_id, TOTP::TOTP_SODIUM_SECRET_KEY, $new_key );
673 // Logging.
674 $this->log( 'Update UID: ' . $user_id . '. Previous state: ' . $state, 'internal' );
675
676 return true;
677 }
678 $this->log( 'Encryption error for UID: ' . $user_id . '. State: ' . $state, 'internal' );
679
680 return false;
681 }
682
683 /**
684 * Old states with plaintext or pub key are exist?
685 *
686 * @param int $user_id The ID of the user.
687 *
688 * @return bool
689 * @throws SodiumException On failure.
690 */
691 public function maybe_update( $user_id ) {
692 $old_key = get_user_meta( $user_id, Totp::TOTP_SECRET_KEY, true );
693 if ( is_string( $old_key ) && '' !== $old_key ) {
694 // Is it a plaintext? It was before v3.3.1.
695 if ( TOTP::TOTP_LENGTH === mb_strlen( $old_key, '8bit' ) ) {
696 return $this->reencrypt_data( $user_id, $old_key );
697 }
698 // Is it encrypted via a pub key? It was before v3.4.0.
699 $decrypted_data = Legacy_Versions::get_decrypted_data_with_pub_key( $old_key );
700 if ( false !== $decrypted_data ) {
701 return $this->reencrypt_data( $user_id, $decrypted_data, 'pub-key' );
702 }
703 }
704
705 return false;
706 }
707
708 /**
709 * Finds whether at least anyone user role in array of the enabled 2FA user roles.
710 *
711 * @param WP_User $user User instance object.
712 * @param array $roles User roles.
713 *
714 * @return bool Return true for at least one role matches else false return.
715 */
716 public function is_auth_enable_for( WP_User $user, array $roles ): bool {
717 if ( false === apply_filters( 'wp_defender_2fa_user_enabled', true, $user->ID ) ) {
718 return false;
719 }
720
721 $common_roles = array_intersect( $this->get_roles( $user ), $roles );
722 return array() !== $common_roles;
723 }
724
725 /**
726 * Remove actions when rendering 2FA screen during login.
727 *
728 * @return void
729 * @since 3.5.0
730 */
731 public function remove_actions_for_2fa_screen(): void {
732 if (
733 class_exists( '\WordfenceLS\Controller_WordfenceLS' ) &&
734 method_exists( '\WordfenceLS\Controller_WordfenceLS', 'shared' ) &&
735 is_object( Controller_WordfenceLS::shared() ) &&
736 method_exists( Controller_WordfenceLS::shared(), '_login_enqueue_scripts' )
737 ) {
738 remove_action(
739 'login_enqueue_scripts',
740 array(
741 Controller_WordfenceLS::shared(),
742 '_login_enqueue_scripts',
743 )
744 );
745 }
746 }
747
748 /**
749 * Queue hooks when this class init.
750 *
751 * @return void
752 */
753 public function add_hooks(): void {
754 // This will be only displayed on a single site and the main site of MU.
755 if ( is_multisite() ) {
756 add_filter( 'wpmu_users_columns', array( $this, 'alter_users_table' ) );
757 add_action( 'network_admin_notices', array( $this, 'admin_notices' ) );
758 add_filter( 'ms_user_row_actions', array( $this, 'display_user_actions' ), 10, 2 );
759 } else {
760 add_filter( 'manage_users_columns', array( $this, 'alter_users_table' ) );
761 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
762 add_filter( 'user_row_actions', array( $this, 'display_user_actions' ), 10, 2 );
763 }
764 add_filter( 'manage_users_custom_column', array( $this, 'alter_user_table_row' ), 10, 3 );
765 add_filter( 'ms_shortcode_ajax_login', array( $this, 'm2_no_ajax' ) );
766 }
767
768 /**
769 * Add a column in the users table, column will be last.
770 *
771 * @param array $columns an array of columns.
772 *
773 * @return array
774 */
775 public function alter_users_table( array $columns ) {
776 $columns = array_slice( $columns, 0, count( $columns ) - 1 )
777 + array( 'defender-two-fa' => esc_html__( 'Two Factor', 'defender-security' ) )
778 + array_slice( $columns, count( $columns ) - 1 );
779
780 return $columns;
781 }
782
783 /**
784 * Reset 2FA methods for specific user and display notice.
785 *
786 * @return void
787 */
788 public function admin_notices() {
789 $screen = get_current_screen();
790 $screen_id = is_multisite() ? 'users-network' : 'users';
791
792 if ( $screen_id !== $screen->id || ! current_user_can( 'edit_users' ) ) {
793 return;
794 }
795 $action = defender_get_data_from_request( 'action', 'g' );
796 $user_id = defender_get_data_from_request( 'user', 'g' );
797 if ( ! isset( $action, $user_id ) ) {
798 return;
799 }
800 if ( 'disable_2fa_methods' !== $action ) {
801 return;
802 }
803 $user = get_user_by( 'id', $user_id );
804 if ( ! is_object( $user ) ) {
805 return;
806 }
807 // Maybe the value has already been cleared.
808 $default_provider = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
809 if ( in_array( $default_provider, array( false, '', array() ), true ) ) {
810 return;
811 }
812 // Default and enabled 2fa providers are cleared for user.
813 update_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, '' );
814 update_user_meta( $user_id, self::ENABLED_PROVIDERS_USER_KEY, '' );
815 ?>
816 <div class="notice notice-success is-dismissible">
817 <p>
818 <?php
819 printf(
820 /* translators: %s: User name. */
821 esc_html__( 'Two factor authentication has been reset for %s.', 'defender-security' ),
822 '<b>' . esc_html( $user->display_name ) . '</b>'
823 );
824 ?>
825 </p>
826 </div>
827 <?php
828 }
829
830 /**
831 * Add the link to reset 2FA settings for specific user. It's without bulk actions.
832 *
833 * @param string[] $actions The array of user actions.
834 * @param WP_User $user WP_User object for the currently listed user.
835 *
836 * @return array
837 */
838 public function display_user_actions( $actions, WP_User $user ) {
839 // Only for users that have one enabled 2fa method at least.
840 $default_provider = get_user_meta( $user->ID, self::DEFAULT_PROVIDER_USER_KEY, true );
841 if ( in_array( $default_provider, array( false, '', array() ), true ) ) {
842 return $actions;
843 }
844
845 $cap = is_multisite() ? 'manage_network_options' : 'manage_options';
846 if ( current_user_can( $cap ) ) {
847 $actions['disable_wpdef_2fa_methods'] = sprintf(
848 '<a href="%s">%s</a>',
849 wp_nonce_url( "users.php?action=disable_2fa_methods&amp;user={$user->ID}", 'bulk-users' ),
850 esc_html__( 'Reset two factor', 'defender-security' )
851 );
852 }
853
854 return $actions;
855 }
856
857 /**
858 * This method is part of the Two_Fa class and is used to alter the user table row value for the specified column.
859 *
860 * @param mixed $val The current value of the user table row.
861 * @param string $column_name The name of the column.
862 * @param int $user_id The ID of the user.
863 *
864 * @return string The altered value of the user table row.
865 */
866 public function alter_user_table_row( $val, string $column_name, int $user_id ) {
867 // @since 3.3.0. Fix an error from other plugins.
868 $val = (string) $val;
869 if ( 'defender-two-fa' !== $column_name ) {
870 return $val;
871 }
872 $provider_slug = get_user_meta( $user_id, self::DEFAULT_PROVIDER_USER_KEY, true );
873 $provider = $this->get_provider_by_slug( $provider_slug );
874 if ( is_wp_error( $provider ) ) {
875 return '';
876 }
877
878 return $provider->get_user_label();
879 }
880
881 /**
882 * Stop ajax login on membership 2.
883 *
884 * @return bool
885 */
886 public function m2_no_ajax() {
887 return false;
888 }
889 }
890