PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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 / controller / two-factor.php

two-factor.php in Defender Security – Malware Scanner, Login Security & Firewall 3.1.0, at src/controller/two-factor.php

1,123 lines 33.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare( strict_types = 1 );
3
4 namespace WP_Defender\Controller;
5
6 use Calotes\Component\Request;
7 use Calotes\Helper\Array_Cache;
8 use Calotes\Helper\HTTP;
9 use Calotes\Helper\Route;
10 use WP_Defender\Behavior\WPMUDEV;
11 use WP_Defender\Component\Config\Config_Hub_Helper;
12 use WP_Defender\Component\Two_Factor\Providers\Webauthn;
13 use WP_Defender\Component\Two_Factor\Providers\Totp;
14 use WP_Defender\Controller;
15 use WP_Defender\Model\Setting\Two_Fa;
16 use Calotes\Component\Response;
17 use WP_Defender\Component\Two_Fa as Two_Fa_Component;
18 use WP_Defender\Component\Two_Factor\Providers\Backup_Codes;
19 use WP_Defender\Component\Two_Factor\Providers\Fallback_Email;
20 use WP_Defender\Traits\Webauthn as Webauthn_Trait;
21
22 class Two_Factor extends Controller {
23 use Webauthn_Trait;
24
25 public $slug = 'wdf-2fa';
26
27 /**
28 * @var Two_Fa
29 */
30 protected $model;
31
32 /**
33 * @var mixed|Two_Fa_Component
34 */
35 protected $service;
36
37 /**
38 * @var array
39 */
40 protected $compatibility_notices = [];
41
42 /**
43 * @var \WP_Defender\Component\Password_Protection
44 */
45 protected $password_protection_service;
46
47 public function __construct() {
48 $title = esc_html__( '2FA', 'wpdef' );
49
50 $this->register_page(
51 $title,
52 $this->slug,
53 [ &$this, 'main_view' ],
54 $this->parent_slug,
55 null,
56 $this->menu_title( $title )
57 );
58 add_action( 'defender_enqueue_assets', [ &$this, 'enqueue_assets' ] );
59 $this->register_routes();
60 $this->service = wd_di()->get( Two_Fa_Component::class );
61 $this->model = wd_di()->get( Two_Fa::class );
62
63 $this->password_protection_service = wd_di()->get( \WP_Defender\Component\Password_Protection::class );
64
65 add_action( 'update_option_jetpack_active_modules', [ &$this, 'listen_for_jetpack_option' ], 10, 3 );
66
67 if ( $this->model->enabled ) {
68 require_once ABSPATH . 'wp-admin/includes/plugin.php';
69 $is_jetpack_sso = $this->service->is_jetpack_sso();
70 $is_tml = $this->service->is_tml();
71 add_action( 'admin_init', [ $this->service, 'get_providers' ] );
72 add_action( 'pre_get_users', [ &$this, 'filter_users_by_2fa' ] );
73 add_action( 'show_user_profile', [ &$this, 'show_user_profile' ] );
74 add_action( 'profile_update', [ &$this, 'profile_update' ] );
75
76 if ( ! defined( 'DOING_AJAX' ) && ! $is_jetpack_sso && ! $is_tml ) {
77 add_filter( 'wp_authenticate_user', [ &$this, 'maybe_show_otp_form' ], 9, 2 );
78 add_action( 'set_logged_in_cookie', [ &$this, 'store_session_key' ] );
79 add_action( 'login_form_defender-verify-otp', [ &$this, 'verify_otp_login_time' ] );
80 } else {
81 if ( $is_jetpack_sso ) {
82 $this->compatibility_notices[] = __( "We've detected a conflict with Jetpack's Wordpress.com Log In feature. Please disable it and return to this page to continue setup.", 'wpdef' );
83 }
84 if ( $is_tml ) {
85 $this->compatibility_notices[] = __( "We've detected a conflict with Theme my login. Please disable it and return to this page to continue setup.", 'wpdef' );
86 }
87 }
88 // Force auth redirect.
89 add_action( 'current_screen', [ &$this, 'maybe_redirect_to_show_2fa_enabler' ], 1 );
90 if ( is_multisite() ) {
91 add_filter( 'wpmu_users_columns', [ &$this, 'alter_users_table' ] );
92 } else {
93 add_filter( 'manage_users_columns', [ &$this, 'alter_users_table' ] );
94 }
95 add_filter( 'manage_users_custom_column', [ &$this, 'alter_user_table_row' ], 10, 3 );
96 add_filter( 'ms_shortcode_ajax_login', [ &$this, 'm2_no_ajax' ] );
97 // Todo: add the verify for filter 'login_redirect'.
98
99 $this->woocommerce_hooks();
100 }
101 }
102
103 /**
104 * Get menu title.
105 *
106 * @since 3.1.0
107 * @retun string
108 */
109 protected function menu_title( $title ) {
110 $info = defender_white_label_status();
111 $suffix = '<span style="padding: 2px 6px;border-radius: 9px;background-color: #17A8E3;color: #FFF;font-size: 8px;letter-spacing: -0.25px;text-transform: uppercase;vertical-align: middle;">' . __( 'NEW', 'wpdef' ) . '</span>';
112 if ( ! $info['hide_doc_link'] ) {
113 $title .= ' ' . $suffix;
114 }
115
116 return $title;
117 }
118
119 /**
120 * We have some feature conflict with jetpack, so listen to know when Defender can on.
121 *
122 * @param $old_value
123 * @param $value
124 * @param $option
125 */
126 public function listen_for_jetpack_option( $old_value, $value, $option ) {
127 if ( false !== array_search( 'sso', $value, true ) ) {
128 $this->model->mark_as_conflict( 'jetpack/jetpack.php' );
129 } else {
130 $this->model->mark_as_un_conflict( 'jetpack/jetpack.php' );
131 }
132 }
133
134 /**
135 * If force redirect enabled, then we should check and redirect to profile page until the 2FA enabled.
136 */
137 public function maybe_redirect_to_show_2fa_enabler() {
138 $user = wp_get_current_user();
139 if ( ! is_object( $user ) ) {
140 return;
141 }
142 // Is User role from common list checked?
143 if ( false === $this->is_auth_enabled_for_user_role( $user ) ) {
144 return;
145 }
146 // Is 'Force Authentication' checked?
147 if ( false === $this->model->force_auth ) {
148 return;
149 }
150 // Is User role from forced list checked?
151 if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
152 return;
153 }
154 // Is TOTP saved with a passcode?
155 if ( ! empty( $this->service->get_available_providers_for_user( $user ) ) ) {
156 return;
157 }
158 $screen = get_current_screen();
159 if ( 'profile' !== $screen->id ) {
160 wp_safe_redirect( admin_url( 'profile.php' ) . '#defender-security' );
161 exit;
162 }
163 }
164
165 /**
166 * Add a column in the users table, column will be last.
167 *
168 * @param array $columns
169 *
170 * @return array
171 */
172 public function alter_users_table( $columns ) {
173 $columns = array_slice( $columns, 0, count( $columns ) - 1 )
174 + [ 'defender-two-fa' => __( 'Two Factor', 'wpdef' ) ]
175 + array_slice( $columns, count( $columns ) - 1 );
176
177 return $columns;
178 }
179
180 /**
181 * @param $val
182 * @param $column_name
183 * @param $user_id
184 *
185 * @return string
186 * @since 2.8.1 Update return value.
187 */
188 public function alter_user_table_row( $val, $column_name, $user_id ) {
189 if ( 'defender-two-fa' !== $column_name ) {
190 return $val;
191 }
192 $provider_slug = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true );
193 $provider = $this->service->get_provider_by_slug( $provider_slug );
194 if ( is_wp_error( $provider ) ) {
195 return '';
196 }
197
198 return $provider->get_user_label();
199 }
200
201 /**
202 * Retrieve the backup code if lost phone.
203 *
204 * @param Request $request
205 *
206 * @return Response
207 * @defender_route
208 * @is_public
209 */
210 public function send_backup_code( Request $request ): Response {
211 $data = $request->get_data();
212 $token = $data['token'];
213 $ret = $this->service->send_otp_to_email( $token );
214 if ( false === $ret ) {
215 return new Response(
216 false,
217 [ 'message' => __( 'Please try again.', 'wpdef' ) ]
218 );
219 }
220
221 if ( is_wp_error( $ret ) ) {
222 return new Response(
223 false,
224 [ 'message' => $ret->get_error_message() ]
225 );
226 }
227
228 return new Response(
229 true,
230 [ 'message' => __( 'Your code has been sent to your email.', 'wpdef' ) ]
231 );
232 }
233
234 /**
235 * Verify the OTP after user login successful.
236 *
237 * @return void
238 */
239 public function verify_otp_login_time() {
240 if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) {
241 return;
242 }
243 if ( empty( $_POST['_wpnonce'] ) ) {
244 return;
245 }
246 if ( ! wp_verify_nonce( $_POST['_wpnonce'], 'verify_otp' ) ) {
247 return;
248 }
249 // Base params.
250 $params = [
251 'password' => $this->password_protection_service->get_submitted_password(),
252 'user_id' => null,
253 ];
254 // Find the user first.
255 $token = HTTP::post( 'login_token' );
256 $query = new \WP_User_Query(
257 [
258 'blog_id' => 0,
259 'meta_key' => 'defender_two_fa_token',
260 'meta_value' => $token,
261 ]
262 );
263
264 if ( 0 === $query->get_total() ) {
265 $params['error'] = new \WP_Error( 'opt_fail', __( 'Invalid request', 'wpdef' ) );
266 $this->render_otp_screen( $params );
267 }
268 $user = $query->get_results()[0];
269 $params['user_id'] = $user->ID;
270 $redirect = HTTP::post( 'redirect_to', admin_url() );
271 $params['token'] = uniqid( 'two_fa' );
272 update_user_meta( $user->ID, 'defender_two_fa_token', $params['token'] );
273 // Get Auth method.
274 $auth_method = HTTP::post( 'auth_method' );
275 if ( empty( $auth_method ) ) {
276 $auth_method = $this->service->get_default_provider_slug_for_user( $user->ID );
277 }
278 $params['default_slug'] = $auth_method;
279 // Get provider object.
280 $provider = $this->service->get_provider_by_slug( $auth_method );
281 if ( is_wp_error( $provider ) ) {
282 $params['error'] = $provider;
283 $this->render_otp_screen( $params );
284 }
285 $result = $provider->validate_authentication( $user );
286 if ( is_wp_error( $result ) ) {
287 $params['error'] = $result;
288 $this->render_otp_screen( $params );
289 }
290 if ( $result ) {
291 // Clean token.
292 delete_user_meta( $user->ID, 'defender_two_fa_token' );
293
294 $password = HTTP::post( 'password' );
295
296 $is_weak_password = $this->password_protection_service->is_weak_password( $user, $password );
297 if ( true === $is_weak_password ) {
298 $this->password_protection_service->do_weak_reset( $user, $password );
299 } elseif ( $this->password_protection_service->is_force_reset( $user ) ) {
300 $this->password_protection_service->do_force_reset( $user, $password );
301 } else {
302 $user_id = $user->ID;
303 // Set active user.
304 wp_set_current_user( $user_id, $user->user_login );
305 // Todo: add code for 'rememberme'-option.
306 wp_set_auth_cookie( $user_id, true );
307
308 /**
309 * Fires after successful login via 2fa.
310 *
311 * @since 2.6.1
312 *
313 * @param int $user_id
314 */
315 do_action( 'wpmu_2fa_login', $user_id );
316
317 if ( isset( $_REQUEST['interim-login'] ) ) {
318 $params['interim_login'] = 'success';
319 $params['message'] = '<p class="message">' . __( 'You have logged in successfully.', 'wpdef' ) . '</p>';
320 $this->render_otp_screen( $params );
321 exit;
322 } else {
323 $redirect = apply_filters(
324 'login_redirect',
325 $redirect,
326 $this->redirect_url(),
327 $user
328 );
329 wp_safe_redirect( $redirect );
330 exit;
331 }
332 }
333 }
334
335 $params['error'] = new \WP_Error(
336 'opt_fail',
337 __( 'Whoops, the passcode you entered was incorrect or expired.', 'wpdef' )
338 );
339 $this->render_otp_screen( $params );
340 }
341
342 /**
343 * Render otp form. Required conditions for the current user:
344 * user data is not empty,
345 * password matches the user,
346 * user role is checked on 2FA settings,
347 * user has at least one 2FA auth method available.
348 *
349 * @param \WP_User $user Object of the logged-in user.
350 * @param string $password Plain password string.
351 */
352 public function maybe_show_otp_form( $user, $password ) {
353 $params = [];
354 if (
355 ! empty( $user ) && ! empty( $password ) && $user instanceof \WP_User
356 && wp_check_password( $password, $user->data->user_pass, $user->ID )
357 && $this->is_auth_enabled_for_user_role( $user )
358 && ! empty( $this->service->get_available_providers_for_user( $user ) )
359 ) {
360 $cookie = Array_Cache::get( 'auth_cookie', '2fa' );
361 if ( null !== $cookie ) {
362 // Clear all session data if any.
363 $session = \WP_Session_Tokens::get_instance( $user->ID );
364 $session->destroy( $cookie['token'] );
365 }
366 // Prevent user to login, and show otp screen.
367 wp_clear_auth_cookie();
368 // All goods, we'll need to create a unique token to mark this user.
369 $params['token'] = uniqid( 'two_fa' );
370 update_user_meta( $user->ID, 'defender_two_fa_token', $params['token'] );
371 $params['password'] = $password;
372 $params['user_id'] = $user->ID;
373 // Get default provider.
374 $params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID );
375 if ( Fallback_Email::$slug === $params['default_slug'] ) {
376 $result = $this->service->send_otp_to_email( $params['token'] );
377 if ( is_wp_error( $result ) ) {
378 $params['error'] = $result;
379 $this->render_otp_screen( $params );
380 }
381 }
382 $this->render_otp_screen( $params );
383 }
384
385 return $user;
386 }
387
388 /**
389 * Render the OTP screen after login successful.
390 *
391 * @param array $params
392 */
393 private function render_otp_screen( $params = [] ) {
394 $user = null;
395 wp_enqueue_script( 'jquery' );
396 wp_enqueue_style( 'defender-otp-screen', defender_asset_url( '/assets/css/otp.css' ) );
397 $params['redirect_to'] = $this->redirect_url();
398 if ( ! isset( $params['error'] ) ) {
399 $params['error'] = null;
400 }
401 // If this goes here then the current user is ok, need to show the 2 auth.
402 $this->attach_behavior( 'wpmudev', WPMUDEV::class );
403 $custom_graphic = '';
404 $settings = new Two_Fa();
405 if ( $this->is_pro() && $settings->custom_graphic && '' !== $settings->custom_graphic_url ) {
406 $custom_graphic = $settings->custom_graphic_url;
407 }
408 $this->detach_behavior( 'wpmudev' );
409 $params['custom_graphic'] = $custom_graphic;
410
411 $routes = $this->dump_routes_and_nonces();
412 $params['providers'] = [];
413 if ( isset( $params['user_id'] ) ) {
414 $user = get_user_by( 'id', $params['user_id'] );
415 if ( is_object( $user ) ) {
416 $params['providers'] = $this->service->get_available_providers_for_user( $user );
417 // Get default provider.
418 if ( empty( $params['default_slug'] ) ) {
419 $params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID );
420 }
421 }
422 }
423 if ( true === array_key_exists( Webauthn::$slug, $params['providers'] ) ) {
424 wp_enqueue_style( 'defender-biometric-login-screen', defender_asset_url( '/assets/css/biometric.css' ), [], DEFENDER_VERSION );
425 wp_enqueue_script(
426 'wpdef_webauthn_common_script',
427 plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ),
428 [],
429 DEFENDER_VERSION,
430 true
431 );
432 wp_enqueue_script(
433 'defender-biometric-login-script',
434 plugins_url( 'assets/js/biometric-login.js', WP_DEFENDER_FILE ),
435 [
436 'jquery',
437 'wpdef_webauthn_common_script',
438 ],
439 DEFENDER_VERSION,
440 true
441 );
442 $webauthn_controller = wd_di()->get( \WP_Defender\Controller\Webauthn::class );
443 wp_localize_script(
444 'defender-biometric-login-script',
445 'webauthn',
446 [
447 'admin_url' => admin_url( 'admin-ajax.php' ),
448 'nonce' => wp_create_nonce( 'wpdef_webauthn' ),
449 'i18n' => $webauthn_controller->get_translations(),
450 'username' => ! empty( $user->user_login ) ? $user->user_login : '',
451 'provider_slug' => Webauthn::$slug,
452 'error' => $params['error'],
453 ]
454 );
455 }
456 $params['action_fallback_email'] = admin_url( 'admin-ajax.php' ) . sprintf(
457 '?action=%s&route=%s&_def_nonce=%s',
458 defender_base_action(),
459 $this->check_route( $routes['routes']['send_backup_code'] ),
460 $routes['nonces']['send_backup_code']
461 );
462 $this->render_partial( 'two-fa/otp', $params );
463 exit;
464 }
465
466 /**
467 * Cache the auth cookie.
468 *
469 * @param $cookie
470 */
471 public function store_session_key( $cookie ) {
472 // Clear login cookie to ensure nonce consistency.
473 if ( ! is_user_logged_in() && isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) {
474 unset( $_COOKIE[ LOGGED_IN_COOKIE ] );
475 }
476
477 $cookie = wp_parse_auth_cookie( $cookie, 'logged_in' );
478 Array_Cache::set( 'auth_cookie', $cookie, '2fa' );
479 }
480
481 /**
482 * Disable 2FA TOTP method for the current user. It's not from the list of routes.
483 *
484 * @return Response
485 * @defender_route
486 * @is_public
487 */
488 public function disable_totp() {
489 $user_id = get_current_user_id();
490 update_user_meta( $user_id, 'defenderAuthOn', 0 );
491 delete_user_meta( $user_id, 'defenderAuthSecret' );
492 // Remove TOTP from enabled providers.
493 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
494 if ( isset( $enabled_providers ) && ! empty( $enabled_providers ) ) {
495 foreach ( $enabled_providers as $key => $slug ) {
496 if ( Totp::$slug === $slug ) {
497 unset( $enabled_providers[ $key ] );
498 break;
499 }
500 }
501 } else {
502 $enabled_providers = '';
503 }
504 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
505
506 return new Response( true, [] );
507 }
508
509 /**
510 * Verify the OTP and enable 2fa, use in profile.php. It's not from the list of routes.
511 *
512 * @param Request $request
513 *
514 * @return void|Response
515 * @defender_route
516 * @is_public
517 */
518 public function verify_otp_for_enabling( Request $request ) {
519 if ( ! is_user_logged_in() ) {
520 return;
521 }
522 $data = $request->get_data();
523 $otp = $data['otp'] ?? false;
524 if ( false === $otp || strlen( $otp ) < 6 ) {
525 return new Response(
526 false,
527 [ 'message' => __( 'Please input a valid OTP code.', 'wpdef' ) ]
528 );
529 }
530 if ( $this->service->verify_otp( $otp ) ) {
531 $user_id = get_current_user_id();
532 $this->service->enable_otp( $user_id );
533 $totp_slug = Totp::$slug;
534 // Add TOTP to enabled providers.
535 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
536 if ( isset( $enabled_providers ) && ! empty( $enabled_providers ) ) {
537 // Array of enabled providers is not empty now.
538 if ( ! in_array( Totp::$slug, $enabled_providers, true ) ) {
539 $enabled_providers[] = $totp_slug;
540 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
541 }
542 } else {
543 // Array of enabled providers is empty now.
544 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, [ $totp_slug ] );
545 }
546 // If no default provider then add TOTP as it.
547 $default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true );
548 if ( empty( $default_provider ) ) {
549 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $totp_slug );
550 }
551
552 return new Response( true, [] );
553 } else {
554 return new Response(
555 false,
556 [ 'message' => __( 'Your OTP code is incorrect. Please try again.', 'wpdef' ) ]
557 );
558 }
559 }
560
561 /**
562 * @param int $user_id
563 */
564 protected function clear_providers( $user_id ) {
565 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' );
566 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, '' );
567 }
568
569 /**
570 * Triggers ONLY when a user is viewing their own profile page.
571 * For all users need to use the hook 'edit_user_profile_update'.
572 *
573 * @param int $user_id
574 */
575 public function profile_update( $user_id ) {
576 if ( isset( $_POST['_wpdef_2fa_nonce_user_options'] ) ) {
577 check_admin_referer( 'wpdef_2fa_user_options', '_wpdef_2fa_nonce_user_options' );
578
579 if (
580 ! isset( $_POST[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] )
581 || ! is_array( $_POST[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] )
582 ) {
583 return;
584 }
585 // Remove empty elements.
586 $checked_providers = array_diff( $_POST[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ], [ '' ] );
587 // If no option is checked then the values for default provider and enabled providers are cleared.
588 if ( empty( $checked_providers ) ) {
589 $this->clear_providers( $user_id );
590
591 return;
592 }
593
594 $providers = $this->service->get_providers();
595 // For Fallback-Email method: the email value should be not empty and valid.
596 if ( in_array( Fallback_Email::$slug, $checked_providers, true ) ) {
597 $email = HTTP::post( 'def_2fa_backup_email' );
598 if ( ! empty( $email ) && filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
599 update_user_meta( $user_id, Fallback_Email::FALLBACK_EMAIL_KEY, $email );
600 } else {
601 unset( $checked_providers[ Fallback_Email::$slug ] );
602 }
603 }
604
605 // For Webauthn method: a user must have at least once device registered.
606 $key = array_search( Webauthn::$slug, $checked_providers, true );
607 if ( false !== $key ) {
608 $user_authenticators = wd_di()->get( \WP_Defender\Controller\Webauthn::class )->get_current_user_authenticators( $user_id );
609 if ( 0 === count( $user_authenticators ) ) {
610 unset( $checked_providers[ $key ] );
611 }
612 }
613 // Case when WebAuthn is checked but no registered devices OR Fallback_Email has an invalid email value.
614 if ( empty( $checked_providers ) ) {
615 $this->clear_providers( $user_id );
616
617 return;
618 }
619
620 // Current user.
621 $user = get_user_by( 'id', $user_id );
622 // Enable only the available providers.
623 $enabled_providers = [];
624 foreach ( $providers as $slug => $provider ) {
625 if ( in_array( $slug, $checked_providers, true ) && $provider->is_available_for_user( $user ) ) {
626 $enabled_providers[] = $slug;
627 }
628 }
629 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
630 // Default provider must be enabled.
631 $default_provider = $_POST[ Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY ] ?? '';
632 // The case#1 when all 2fa providers were deactivated before.
633 if ( empty( $default_provider ) ) {
634 $default_provider = $enabled_providers[0];
635 }
636 // The case#2 when prev default provider is deactivated and another one is activated.
637 if ( ! in_array( $default_provider, $checked_providers, true ) ) {
638 $default_provider = $enabled_providers[0];
639 }
640 // Save default provider.
641 update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $default_provider );
642 }
643 }
644
645 /**
646 * Check if DEFENDER_DEBUG is enabled for the route.
647 *
648 * @param string $route
649 *
650 * @return string
651 */
652 public function check_route( $route ) {
653 return defined( 'DEFENDER_DEBUG' ) && true === constant( 'DEFENDER_DEBUG' )
654 ? wp_slash( $route )
655 : $route;
656 }
657
658 /**
659 * A simple filter to show activate 2fa screen on profile page.
660 *
661 * @param object $user \WP_User object
662 *
663 * @return void
664 */
665 public function show_user_profile( $user ) {
666 if ( empty( $user->roles ) ) {
667 return;
668 }
669 // This method is better than is_intersected_arrays() because it is flexibly controlled with a nested hook.
670 if ( ! $this->is_auth_enabled_for_user_role( $user ) ) {
671 return;
672 }
673 wp_enqueue_style( 'defender-profile-2fa', defender_asset_url( '/assets/css/two-factor.css' ) );
674
675 $webauthn_controller = wd_di()->get( \WP_Defender\Controller\Webauthn::class );
676 $webauthn_requirements = $this->check_webauthn_requirements();
677 if ( $this->service->is_checked_enabled_provider_by_slug( $user, Webauthn::$slug ) && ! $webauthn_requirements ) {
678 $this->service->remove_enabled_provider_for_user( Webauthn::$slug, $user );
679 }
680
681 wp_enqueue_script(
682 'wpdef_webauthn_common_script',
683 plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ),
684 [],
685 DEFENDER_VERSION,
686 true
687 );
688 wp_enqueue_script(
689 'wpdef_webauthn_script',
690 plugins_url( 'assets/js/webauthn.js', WP_DEFENDER_FILE ),
691 [
692 'jquery',
693 'wpdef_webauthn_common_script',
694 ],
695 DEFENDER_VERSION,
696 true
697 );
698 $user = wp_get_current_user();
699 wp_localize_script(
700 'wpdef_webauthn_script',
701 'webauthn',
702 [
703 'admin_url' => admin_url( 'admin-ajax.php' ),
704 'nonce' => wp_create_nonce( 'wpdef_webauthn' ),
705 'i18n' => $webauthn_controller->get_translations(),
706 'registered_auths' => $webauthn_controller->get_current_user_authenticators( $user->ID ),
707 'username' => ! empty( $user->user_login ) ? $user->user_login : '',
708 ]
709 );
710
711 $forced_auth = $this->service->is_intersected_arrays( $user->roles, $this->model->force_auth_roles );
712 $default_values = $this->model->get_default_values();
713 $enabled_providers = $this->service->get_available_providers_for_user( $user );
714 $enabled_provider_slugs = ! empty( $enabled_providers ) ? array_keys( $enabled_providers ) : [];
715 $default_provider_slug = $this->service->get_default_provider_slug_for_user( $user->ID );
716 $webauthn_enabled = $this->service->is_checked_enabled_provider_by_slug( $user, Webauthn::$slug );
717
718 $this->render_partial(
719 'two-fa/user-options',
720 [
721 'is_force_auth' => $forced_auth && $this->model->force_auth && empty( $enabled_providers ),
722 'force_auth_message' => $this->model->force_auth_mess,
723 'default_message' => $default_values['message'],
724 'user' => $user,
725 'all_providers' => $this->service->get_providers(),
726 'enabled_providers_key' => Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY,
727 'default_provider_key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
728 'checked_provider_slugs' => $enabled_provider_slugs,
729 'checked_def_provider_slug' => ! empty( $default_provider_slug ) ? $default_provider_slug : null,
730 'webauthn_requirements' => $webauthn_requirements,
731 'webauthn_enabled' => $webauthn_enabled,
732 'webauthn_slug' => Webauthn::$slug,
733 ]
734 );
735 }
736
737 /**
738 * Save settings.
739 *
740 * @param Request $request
741 *
742 * @return Response
743 * @defender_route
744 */
745 public function save_settings( Request $request ): Response {
746 $model = $this->model;
747 $data = $request->get_data();
748 $model->import( $data );
749 if ( $model->validate() ) {
750 $model->save();
751 Config_Hub_Helper::set_clear_active_flag();
752
753 return new Response(
754 true,
755 array_merge(
756 [ 'message' => __( 'Your settings have been updated.', 'wpdef' ) ],
757 $this->data_frontend()
758 )
759 );
760 }
761
762 return new Response(
763 false,
764 [ 'message' => $model->get_formatted_errors() ]
765 );
766 }
767
768 /**
769 * @throws \ReflectionException
770 */
771 public function enqueue_assets() {
772 if ( ! $this->is_page_active() ) {
773 return;
774 }
775 wp_enqueue_script( 'clipboard' );
776 wp_enqueue_media();
777 wp_localize_script( 'def-2fa', 'two_fa', $this->data_frontend() );
778 wp_enqueue_script( 'def-2fa' );
779 $this->enqueue_main_assets();
780 }
781
782 /**
783 * Send test email, use in settings screen.
784 *
785 * @param Request $request
786 *
787 * @return Response
788 * @defender_route
789 */
790 public function send_test_email( Request $request ): Response {
791 $data = $request->get_data(
792 [
793 'email_subject' => [
794 'type' => 'string',
795 'sanitize' => 'sanitize_text_field',
796 ],
797 'email_sender' => [
798 'type' => 'string',
799 'sanitize' => 'sanitize_text_field',
800 ],
801 'email_body' => [
802 'type' => 'string',
803 'sanitize' => 'wp_kses_post',
804 ],
805 ]
806 );
807
808 $subject = $data['email_subject'];
809 $sender = $data['email_sender'];
810 $body = $this->render_partial(
811 'email/2fa-lost-phone',
812 [
813 'body' => nl2br( $data['email_body'] ),
814 ],
815 false
816 );
817
818 $params = [
819 'passcode' => '[a-sample-passcode]',
820 'display_name' => $this->get_user_display( get_current_user_id() ),
821 ];
822
823 foreach ( $params as $key => $param ) {
824 $body = str_replace( "{{{$key}}}", $param, $body );
825 }
826 $headers = [ 'Content-Type: text/html; charset=UTF-8' ];
827 if ( $sender ) {
828 $from_email = get_bloginfo( 'admin_email' );
829 $headers[] = sprintf( 'From: %s <%s>', $sender, $from_email );
830 }
831 // Main email template.
832 $body = $this->render_partial(
833 'email/index',
834 [
835 'title' => __( 'Two-Factor Authentication', 'wpdef' ),
836 'content_body' => $body,
837 ],
838 false
839 );
840
841 $send_mail = wp_mail( Fallback_Email::get_backup_email(), $subject, $body, $headers );
842 if ( $send_mail ) {
843 return new Response(
844 true,
845 [ 'message' => __( 'Test email has been sent to your email.', 'wpdef' ) ]
846 );
847 } else {
848 return new Response(
849 false,
850 [ 'message' => __( 'Test email failed.', 'wpdef' ) ]
851 );
852 }
853 }
854
855 /**
856 * @return array
857 */
858 public function to_array() {
859 $settings = new Two_Fa();
860 [$routes, $nonces] = Route::export_routes( 'two_fa' );
861
862 return [
863 'enabled' => $settings->enabled,
864 'useable' => $settings->enabled && count( $settings->user_roles ),
865 'nonces' => $nonces,
866 'endpoints' => $routes,
867 ];
868 }
869
870 public function main_view() {
871 $this->render( 'main' );
872 }
873
874 public function remove_settings() {
875 ( new Two_Fa() )->delete();
876 }
877
878 /*
879 * Remove all users meta. Keys need to remove:
880 * defenderAuthEmail, defenderAuthOn, defenderForceAuth, defenderBackupCode, defenderAuthSecret,
881 * Two_Fa::DEFAULT_PROVIDER_USER_KEY, Two_Fa::ENABLED_PROVIDERS_USER_KEY,
882 * Backup_Codes::BACKUP_CODE_VALUES, Backup_Codes::BACKUP_CODE_START.
883 */
884 public function remove_data() {
885 global $wpdb;
886 $sql = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN ('defenderAuthEmail','defenderAuthOn','defenderForceAuth','defenderBackupCode','defenderAuthSecret', 'wd_2fa_default_provider', 'wd_2fa_enabled_providers', 'wd_2fa_backup_codes', 'wd_2fa_backup_codes_is_activated');";
887 $wpdb->query( $sql );
888 }
889
890 /**
891 * Filter users by 2FA option.
892 */
893 public function filter_users_by_2fa( $query ) {
894 global $pagenow;
895
896 if ( is_admin()
897 && 'users.php' === $pagenow
898 && isset( $_GET['wpdef_two_fa'] )
899 && 'enabled' === sanitize_text_field( $_GET['wpdef_two_fa'] )
900 ) {
901 $query->set(
902 'meta_query',
903 [
904 [
905 'key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY,
906 'value' => array_keys( $this->service->get_providers() ),
907 'compare' => 'IN',
908 ],
909 ]
910 );
911 }
912 }
913
914 /**
915 * All the variables that we will show on frontend, both in the main page, or dashboard widget.
916 *
917 * @return array
918 */
919 public function data_frontend(): array {
920 return array_merge(
921 [
922 'model' => $this->model->export(),
923 'all_roles' => wp_list_pluck( get_editable_roles(), 'name' ),
924 'count' => $this->service->count_users_with_enabled_2fa(),
925 'notices' => $this->compatibility_notices,
926 'new_feature' => '<span class="sui-tag sui-tag-beta margin-right-10">' . __( 'Beta', 'wpdef' ) . '</span>'
927 . sprintf(
928 /* translators: %s: link */
929 __( 'Web Authentication is now available. <a target="_blank" href="%s">Click here</a> to find out more.', 'wpdef' ),
930 'https://wpmudev.com/docs/wpmu-dev-plugins/defender/#web-authentication'
931 ),
932 ],
933 $this->dump_routes_and_nonces()
934 );
935 }
936
937 /**
938 * @param array $data
939 */
940 public function import_data( $data ) {
941 $model = new Two_Fa();
942
943 $model->import( $data );
944 /**
945 * Sometime, the custom image broken when import, when that happen, we will fallback
946 * into default image.
947 */
948 $model->custom_graphic_url = $this->service->get_custom_graphic_url( $model->custom_graphic_url );
949 if ( $model->validate() ) {
950 $model->save();
951 }
952 }
953
954 /**
955 * @return array
956 */
957 public function export_strings() {
958 $settings = new Two_Fa();
959
960 return [
961 $settings->enabled ? __( 'Active', 'wpdef' ) : __( 'Inactive', 'wpdef' ),
962 ];
963 }
964
965 /**
966 * @param array $config
967 * @param bool $is_pro
968 *
969 * @return array
970 */
971 public function config_strings( $config, $is_pro ) {
972
973 return [
974 $config['enabled'] ? __( 'Active', 'wpdef' ) : __( 'Inactive', 'wpdef' ),
975 ];
976 }
977
978 /**
979 * Stop ajax login on membership 2.
980 *
981 * @return bool
982 */
983 public function m2_no_ajax() {
984 return false;
985 }
986
987 /**
988 * WooCommerce prevents any user who cannot 'edit_posts' (subscribers, customers etc.) from accessing admin.
989 * Here we are disabling WooCommerce default behavior, if force 2FA is enabled.
990 *
991 * @param bool $prevent Prevent admin access.
992 */
993 public function handle_woocommerce_prevent_admin_access( $prevent ) {
994 $user = wp_get_current_user();
995 if ( ! is_object( $user ) ) {
996 return;
997 }
998 // Is User role from common list checked?
999 if ( false === $this->is_auth_enabled_for_user_role( $user ) ) {
1000 return $prevent;
1001 }
1002 // Is 'Force Authentication' checked?
1003 if ( false === $this->model->force_auth ) {
1004 return $prevent;
1005 }
1006 // Is User role from forced list checked?
1007 if ( $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
1008 return false;
1009 }
1010 // Is TOTP saved with a passcode?
1011 if ( ! empty( $this->service->get_available_providers_for_user( $user ) ) ) {
1012 return $prevent;
1013 }
1014
1015 return $prevent;
1016 }
1017
1018 /**
1019 * WooCommerce specific hooks.
1020 *
1021 * @return void
1022 */
1023 private function woocommerce_hooks() {
1024 // This filter added only for disable WooCommerce default behavior.
1025 add_filter( 'woocommerce_prevent_admin_access', [ $this, 'handle_woocommerce_prevent_admin_access' ], 10, 1 );
1026
1027 // Handle WooCommerce MyAccount page login redirect.
1028 add_filter( 'woocommerce_login_redirect', [ $this, 'handle_woocommerce_login_redirect' ], 10, 2 );
1029
1030 add_action( 'woocommerce_login_form_end', [ $this, 'add_redirect_to_input' ] );
1031 }
1032
1033 /**
1034 * WooCommerce by default redirect users to My-account page.
1035 * Here we are checking force 2FA is enabled or not.
1036 *
1037 * @param string $redirect Redirect URL.
1038 * @param \WP_User $user Logged-in user.
1039 *
1040 * @return void
1041 */
1042 public function handle_woocommerce_login_redirect( $redirect, $user ) {
1043 // Is User role from common list checked?
1044 if ( false === $this->is_auth_enabled_for_user_role( $user ) ) {
1045 return $redirect;
1046 }
1047 // Is 'Force Authentication' checked?
1048 if ( false === $this->model->force_auth ) {
1049 return $redirect;
1050 }
1051 // Is User role from forced list checked?
1052 if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) {
1053 return $redirect;
1054 }
1055 // Is TOTP saved with a passcode?
1056 if ( empty( $this->service->get_available_providers_for_user( $user ) ) ) {
1057 return admin_url( 'profile.php' ) . '#defender-security';
1058 }
1059
1060 return $redirect;
1061 }
1062
1063 /**
1064 * Finds whether at least anyone user role in enabled 2FA user roles array.
1065 *
1066 * @param \WP_User $user User instance object.
1067 *
1068 * @return bool Return true for at least one role matches else false return.
1069 */
1070 private function is_auth_enabled_for_user_role( \WP_User $user ): bool {
1071 /**
1072 * Filter 2FA option for a specific user.
1073 *
1074 * @param bool $flag
1075 * @param string $user_id
1076 */
1077 if ( false === apply_filters( 'wp_defender_2fa_user_enabled', true, $user->ID ) ) {
1078 return false;
1079 }
1080 return ! empty( array_intersect( $user->roles, $this->model->user_roles ) );
1081 }
1082
1083 /**
1084 * Return redirect URL after 2FA submit.
1085 */
1086 private function redirect_url() {
1087 return HTTP::post( 'redirect_to', defender_get_request_url() );
1088 }
1089
1090 /**
1091 * Adds redirect_to hidden input to Woo login.
1092 */
1093 public function add_redirect_to_input() {
1094 echo '<input type="hidden" name="redirect_to" value="' . defender_get_request_url() . '">';
1095 }
1096
1097 /**
1098 * Generate Backup codes on Profile page.
1099 *
1100 * @return Response
1101 * @defender_route
1102 * @is_public
1103 */
1104 public function generate_backup_codes(): Response {
1105 $user = wp_get_current_user();
1106
1107 return new Response(
1108 true,
1109 [
1110 'codes' => Backup_Codes::generate_codes( $user ),
1111 'count' => Backup_Codes::display_number_of_codes( Backup_Codes::get_unused_codes_for_user( $user ) ),
1112 'title' => sprintf(
1113 /* translators: %s: count */
1114 __( 'Defender Backup Codes for %s:', 'wpdef' ),
1115 network_site_url()
1116 ),
1117 'button_text' => __( 'Get New Codes', 'wpdef' ),
1118 'description' => __( 'Each backup code can only be used to log in once.', 'wpdef' ),
1119 ]
1120 );
1121 }
1122 }
1123