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

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