| 1 |
<?php |
| 2 |
/** |
| 3 |
* This class handles all the routes related to two-factor authentication. |
| 4 |
* |
| 5 |
* @package WP_Defender\Controller |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace WP_Defender\Controller; |
| 9 |
|
| 10 |
use WP_User; |
| 11 |
use WP_Error; |
| 12 |
use Exception; |
| 13 |
use WP_User_Query; |
| 14 |
use SodiumException; |
| 15 |
use WP_Defender\Event; |
| 16 |
use WP_Session_Tokens; |
| 17 |
use Calotes\Helper\HTTP; |
| 18 |
use Calotes\Helper\Route; |
| 19 |
use Calotes\Component\Request; |
| 20 |
use Calotes\Component\Response; |
| 21 |
use Calotes\Helper\Array_Cache; |
| 22 |
use WP_Defender\Component\Crypt; |
| 23 |
use WP_Defender\Behavior\WPMUDEV; |
| 24 |
use WP_Defender\Model\Setting\Two_Fa; |
| 25 |
use WP_Defender\Integrations\Woocommerce; |
| 26 |
use WP_Defender\Traits\Webauthn as Webauthn_Trait; |
| 27 |
use WP_Defender\Component\Config\Config_Hub_Helper; |
| 28 |
use WP_Defender\Component\Two_Fa as Two_Fa_Component; |
| 29 |
use WP_Defender\Component\Two_Factor\Providers\Totp; |
| 30 |
use WP_Defender\Component\Two_Factor\Providers\Webauthn; |
| 31 |
use WP_Defender\Component\Webauthn as Webauthn_Component; |
| 32 |
use WP_Defender\Controller\Webauthn as Webauthn_Controller; |
| 33 |
use WP_Defender\Component\Two_Factor\Providers\Backup_Codes; |
| 34 |
use WP_Defender\Component\Two_Factor\Providers\Fallback_Email; |
| 35 |
use WP_Defender\Component\Password_Protection as Password_Protection_Service; |
| 36 |
|
| 37 |
/** |
| 38 |
* Handles all the routes related to two-factor authentication. |
| 39 |
*/ |
| 40 |
class Two_Factor extends Event { |
| 41 |
|
| 42 |
use Webauthn_Trait; |
| 43 |
|
| 44 |
/** |
| 45 |
* The slug identifier for this controller. |
| 46 |
* |
| 47 |
* @var string |
| 48 |
*/ |
| 49 |
public $slug = 'wdf-2fa'; |
| 50 |
|
| 51 |
/** |
| 52 |
* The model for handling the data. |
| 53 |
* |
| 54 |
* @var Two_Fa |
| 55 |
*/ |
| 56 |
protected $model; |
| 57 |
|
| 58 |
/** |
| 59 |
* Service for handling logic. |
| 60 |
* |
| 61 |
* @var Two_Fa_Component |
| 62 |
*/ |
| 63 |
protected $service; |
| 64 |
|
| 65 |
/** |
| 66 |
* Compatibility notices for the frontend. |
| 67 |
* |
| 68 |
* @var array |
| 69 |
*/ |
| 70 |
protected $compatibility_notices = array(); |
| 71 |
|
| 72 |
/** |
| 73 |
* Logic for handling the password protection. |
| 74 |
* |
| 75 |
* @var Password_Protection_Service |
| 76 |
*/ |
| 77 |
protected $password_protection_service; |
| 78 |
|
| 79 |
/** |
| 80 |
* Is the woocommerce plugin activated. |
| 81 |
* |
| 82 |
* @var bool |
| 83 |
*/ |
| 84 |
protected $is_woo_activated; |
| 85 |
/** |
| 86 |
* The current logged-in user. |
| 87 |
* |
| 88 |
* @var WP_User |
| 89 |
*/ |
| 90 |
protected $current_user; |
| 91 |
|
| 92 |
/** |
| 93 |
* Defender flush rules slug. |
| 94 |
* |
| 95 |
* @var string |
| 96 |
*/ |
| 97 |
private $flush_slug = 'defender_flush_rules'; |
| 98 |
|
| 99 |
/** |
| 100 |
* Initializes the model and service, registers routes, and sets up scheduled events if the model is active. |
| 101 |
*/ |
| 102 |
public function __construct() { |
| 103 |
$this->register_routes(); |
| 104 |
$this->service = wd_di()->get( Two_Fa_Component::class ); |
| 105 |
$this->model = wd_di()->get( Two_Fa::class ); |
| 106 |
$this->password_protection_service = wd_di()->get( Password_Protection_Service::class ); |
| 107 |
$this->is_woo_activated = wd_di()->get( Woocommerce::class )->is_activated(); |
| 108 |
|
| 109 |
add_action( 'update_option_jetpack_active_modules', array( $this, 'listen_for_jetpack_option' ), 10, 2 ); |
| 110 |
|
| 111 |
if ( $this->model->is_active() ) { |
| 112 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 113 |
$is_jetpack_sso = $this->service->is_jetpack_sso(); |
| 114 |
$is_tml = $this->service->is_tml(); |
| 115 |
add_action( 'admin_init', array( $this, 'load_providers' ) ); |
| 116 |
add_action( 'pre_get_users', array( $this, 'filter_users_by_2fa' ) ); |
| 117 |
add_action( 'show_user_profile', array( $this, 'show_user_profile' ) ); |
| 118 |
add_action( 'profile_update', array( $this, 'profile_update' ) ); |
| 119 |
|
| 120 |
if ( ! defined( 'DOING_AJAX' ) && ! $is_jetpack_sso && ! $is_tml ) { |
| 121 |
add_filter( 'authenticate', array( $this, 'maybe_show_otp_form' ), 30, 3 ); |
| 122 |
add_action( 'set_logged_in_cookie', array( $this, 'store_session_key' ) ); |
| 123 |
add_action( 'login_form_defender-verify-otp', array( $this, 'verify_otp_login_time' ) ); |
| 124 |
} else { |
| 125 |
if ( $is_jetpack_sso ) { |
| 126 |
$this->compatibility_notices[] = esc_html__( |
| 127 |
'We`ve detected a conflict with Jetpack`s Wordpress.com Log In feature. Please disable it and return to this page to continue setup.', |
| 128 |
'defender-security' |
| 129 |
); |
| 130 |
} |
| 131 |
if ( $is_tml ) { |
| 132 |
$this->compatibility_notices[] = esc_html__( |
| 133 |
'We`ve detected a conflict with Theme my login. Please disable it and return to this page to continue setup.', |
| 134 |
'defender-security' |
| 135 |
); |
| 136 |
} |
| 137 |
} |
| 138 |
// Force auth redirect for admin area. |
| 139 |
add_action( 'current_screen', array( $this, 'maybe_redirect_to_show_2fa_enabler' ), 1 ); |
| 140 |
|
| 141 |
$this->service->add_hooks(); |
| 142 |
|
| 143 |
// Todo: add the verify for filter 'login_redirect'. |
| 144 |
if ( $this->is_woo_activated ) { |
| 145 |
// Todo: move to Woocommerce class. |
| 146 |
$this->current_user = wp_get_current_user(); |
| 147 |
$this->woocommerce_hooks(); |
| 148 |
|
| 149 |
// Display 2FA content on Woo My Account page for enabled user roles. |
| 150 |
if ( $this->model->detect_woo && is_object( $this->current_user ) && $this->current_user->exists() |
| 151 |
&& $this->service->is_auth_enable_for( $this->current_user, $this->model->user_roles ) |
| 152 |
) { |
| 153 |
// Show a new Woo submenu. |
| 154 |
add_action( 'init', array( $this, 'wp_defender_2fa_endpoint' ) ); |
| 155 |
add_filter( 'query_vars', array( $this, 'wp_defender_2fa_query_vars' ), 0 ); |
| 156 |
add_filter( 'woocommerce_account_menu_items', array( $this, 'wp_defender_2fa_link_my_account' ) ); |
| 157 |
add_action( |
| 158 |
"woocommerce_account_{$this->slug}_endpoint", |
| 159 |
array( |
| 160 |
& |
| 161 |
$this, |
| 162 |
'wp_defender_2fa_content', |
| 163 |
) |
| 164 |
); |
| 165 |
// Display Woo content for 2FA user settings. |
| 166 |
add_shortcode( 'wp_defender_2fa_user_settings', array( $this, 'display_2fa_user_settings' ) ); |
| 167 |
// Form processing. |
| 168 |
add_action( 'template_redirect', array( $this, 'save_2fa_details' ) ); |
| 169 |
} |
| 170 |
} |
| 171 |
// Fires when 2FA methods are enabled. |
| 172 |
add_action( 'wd_2fa_enabled_provider_slugs', array( $this, 'enable_provider_slugs' ) ); |
| 173 |
} |
| 174 |
} |
| 175 |
|
| 176 |
/** |
| 177 |
* Loads the available service providers. |
| 178 |
* |
| 179 |
* @return void |
| 180 |
*/ |
| 181 |
public function load_providers(): void { |
| 182 |
$this->service->get_providers(); |
| 183 |
} |
| 184 |
|
| 185 |
/** |
| 186 |
* Checks if WooCommerce integration is enabled. |
| 187 |
* |
| 188 |
* @return bool Whether WooCommerce integration is enabled. |
| 189 |
*/ |
| 190 |
public function woo_integration_enabled(): bool { |
| 191 |
return $this->is_woo_activated && $this->model->detect_woo; |
| 192 |
} |
| 193 |
|
| 194 |
/** |
| 195 |
* We have some feature conflict with jetpack, so listen to know when Defender can on. |
| 196 |
* |
| 197 |
* @param mixed $old_value Old value of `jetpack_active_modules`. |
| 198 |
* @param mixed $value New value of `jetpack_active_modules`. |
| 199 |
* |
| 200 |
* @return void |
| 201 |
*/ |
| 202 |
public function listen_for_jetpack_option( $old_value, $value ): void { |
| 203 |
if ( false !== array_search( 'sso', $value, true ) ) { |
| 204 |
$this->model->mark_as_conflict( 'jetpack/jetpack.php' ); |
| 205 |
} else { |
| 206 |
$this->model->mark_as_un_conflict( 'jetpack/jetpack.php' ); |
| 207 |
} |
| 208 |
} |
| 209 |
|
| 210 |
/** |
| 211 |
* If force redirect enabled, then we should check and redirect to profile page until the 2FA enabled. |
| 212 |
* |
| 213 |
* @return void |
| 214 |
*/ |
| 215 |
public function maybe_redirect_to_show_2fa_enabler() { |
| 216 |
$user = wp_get_current_user(); |
| 217 |
if ( ! is_object( $user ) ) { |
| 218 |
return; |
| 219 |
} |
| 220 |
// Is User role from common list checked? |
| 221 |
if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) { |
| 222 |
return; |
| 223 |
} |
| 224 |
// Is 'Force Authentication' checked? |
| 225 |
if ( false === $this->model->force_auth ) { |
| 226 |
return; |
| 227 |
} |
| 228 |
// Is User role from forced list checked? |
| 229 |
if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) { |
| 230 |
return; |
| 231 |
} |
| 232 |
// Is TOTP saved with a passcode? |
| 233 |
if ( array() !== $this->service->get_available_providers_for_user( $user ) ) { |
| 234 |
return; |
| 235 |
} |
| 236 |
$screen = get_current_screen(); |
| 237 |
if ( 'profile' !== $screen->id ) { |
| 238 |
wp_safe_redirect( admin_url( 'profile.php' ) . '#defender-security' ); |
| 239 |
exit; |
| 240 |
} |
| 241 |
} |
| 242 |
|
| 243 |
/** |
| 244 |
* Retrieve the backup code if lost phone. |
| 245 |
* |
| 246 |
* @param Request $request Request object. |
| 247 |
* |
| 248 |
* @return Response |
| 249 |
* @defender_route |
| 250 |
* @is_public |
| 251 |
*/ |
| 252 |
public function send_backup_code( Request $request ): Response { |
| 253 |
$data = $request->get_data(); |
| 254 |
$token = $data['token']; |
| 255 |
$user_id = (int) $data['requested_user']; |
| 256 |
$ret = $this->service->send_otp_to_email( $token, $user_id ); |
| 257 |
if ( false === $ret ) { |
| 258 |
return new Response( |
| 259 |
false, |
| 260 |
array( 'message' => esc_html__( 'Please try again.', 'defender-security' ) ) |
| 261 |
); |
| 262 |
} |
| 263 |
|
| 264 |
if ( is_wp_error( $ret ) ) { |
| 265 |
return new Response( |
| 266 |
false, |
| 267 |
array( 'message' => $ret->get_error_message() ) |
| 268 |
); |
| 269 |
} |
| 270 |
|
| 271 |
return new Response( |
| 272 |
true, |
| 273 |
array( 'message' => esc_html__( 'Your code has been sent to your email.', 'defender-security' ) ) |
| 274 |
); |
| 275 |
} |
| 276 |
|
| 277 |
/** |
| 278 |
* Verify the OTP after user login successful. |
| 279 |
* |
| 280 |
* @return void |
| 281 |
*/ |
| 282 |
public function verify_otp_login_time() { |
| 283 |
if ( 'POST' !== defender_get_data_from_request( 'REQUEST_METHOD', 's' ) ) { |
| 284 |
return; |
| 285 |
} |
| 286 |
|
| 287 |
$post = defender_get_data_from_request( null, 'p' ); |
| 288 |
if ( '' === $post['_wpnonce'] || ! wp_verify_nonce( $post['_wpnonce'], 'verify_otp' ) ) { |
| 289 |
wp_die( esc_html__( 'Nonce verification failed.', 'defender-security' ) ); |
| 290 |
} |
| 291 |
|
| 292 |
$token = HTTP::post( 'login_token' ); |
| 293 |
$user_id = (int) HTTP::post( 'requested_user', 0 ); |
| 294 |
$auth_method = HTTP::post( 'auth_method' ); |
| 295 |
$password = HTTP::post( 'password' ); |
| 296 |
if ( '' === $token || 0 === $user_id || '' === $auth_method || '' === $password ) { |
| 297 |
wp_die( esc_html__( 'Missing parameter(s)', 'defender-security' ) ); |
| 298 |
} |
| 299 |
|
| 300 |
$user = get_user_by( 'id', $user_id ); |
| 301 |
// Spoofed data? E.g. a hidden field user is changed. |
| 302 |
if ( ! is_object( $user ) ) { |
| 303 |
wp_die( esc_html__( 'Invalid user.', 'defender-security' ) ); |
| 304 |
} |
| 305 |
|
| 306 |
$hashed_token = get_user_meta( $user_id, Two_Fa_Component::TOKEN_USER_KEY, true ); |
| 307 |
// Spoofed data again? |
| 308 |
if ( ! Crypt::compare_lines( $hashed_token, wp_hash( $user_id . $token ) ) ) { |
| 309 |
wp_die( esc_html__( 'Invalid request.', 'defender-security' ) ); |
| 310 |
} |
| 311 |
|
| 312 |
// Base params. |
| 313 |
$params = array( |
| 314 |
'password' => $this->password_protection_service->get_submitted_password(), |
| 315 |
'user_id' => $user->ID, |
| 316 |
'token' => $this->get_token( $user_id ), |
| 317 |
'default_slug' => $auth_method, |
| 318 |
); |
| 319 |
|
| 320 |
// Get provider object. |
| 321 |
$provider = $this->service->get_provider_by_slug( $auth_method ); |
| 322 |
if ( is_wp_error( $provider ) ) { |
| 323 |
$params['error'] = $provider; |
| 324 |
$this->render_otp_screen( $params ); |
| 325 |
} |
| 326 |
$result = $provider->validate_authentication( $user ); |
| 327 |
if ( is_wp_error( $result ) ) { |
| 328 |
$params['error'] = $result; |
| 329 |
$this->render_otp_screen( $params ); |
| 330 |
} |
| 331 |
if ( $result ) { |
| 332 |
// Clean token. |
| 333 |
delete_user_meta( $user->ID, Two_Fa_Component::TOKEN_USER_KEY ); |
| 334 |
|
| 335 |
$is_weak_password = $this->password_protection_service->is_weak_password( $user, $password ); |
| 336 |
if ( true === $is_weak_password ) { |
| 337 |
$this->password_protection_service->do_weak_reset( $user, $password ); |
| 338 |
} elseif ( $this->password_protection_service->is_force_reset( $user ) ) { |
| 339 |
$this->password_protection_service->do_force_reset( $user, $password ); |
| 340 |
} else { |
| 341 |
$user_id = $user->ID; |
| 342 |
// For the Webauthn method, the check occurs inside Webauthn_Controller::verify_response(). |
| 343 |
// Set active user. |
| 344 |
wp_set_current_user( $user_id, $user->user_login ); |
| 345 |
// Todo: add code for 'rememberme'-option. |
| 346 |
wp_set_auth_cookie( $user_id, true ); |
| 347 |
|
| 348 |
/** |
| 349 |
* Fires after successful login via 2fa. |
| 350 |
* |
| 351 |
* @param int $user_id @since 2.6.1 |
| 352 |
* @param string $auth_method @since 3.4.0 |
| 353 |
*/ |
| 354 |
do_action( 'wpmu_2fa_login', $user_id, $auth_method ); |
| 355 |
|
| 356 |
$interim_login = defender_get_data_from_request( 'interim-login', 'r' ); |
| 357 |
if ( is_string( $interim_login ) && '' !== trim( $interim_login ) ) { |
| 358 |
$params['interim_login'] = 'success'; |
| 359 |
$params['message'] = '<p class="message">' . esc_html__( 'You have logged in successfully.', 'defender-security' ) . '</p>'; |
| 360 |
$this->render_otp_screen( $params ); |
| 361 |
exit; |
| 362 |
} else { |
| 363 |
// Usual success. |
| 364 |
$redirect = apply_filters( |
| 365 |
'login_redirect', |
| 366 |
HTTP::post( 'redirect_to', admin_url() ), |
| 367 |
$this->redirect_url(), |
| 368 |
$user |
| 369 |
); |
| 370 |
wp_safe_redirect( $redirect ); |
| 371 |
exit; |
| 372 |
} |
| 373 |
} |
| 374 |
} |
| 375 |
$lockout_message = $this->service->verify_attempt( $user->ID, Totp::$slug ); |
| 376 |
|
| 377 |
$params['error'] = new WP_Error( |
| 378 |
'opt_fail', |
| 379 |
'' === trim( $lockout_message ) |
| 380 |
? esc_html__( 'Whoops, the passcode you entered was incorrect or expired.', 'defender-security' ) |
| 381 |
: $lockout_message |
| 382 |
); |
| 383 |
$this->render_otp_screen( $params ); |
| 384 |
exit; |
| 385 |
} |
| 386 |
|
| 387 |
/** |
| 388 |
* Generate a unique token for user. |
| 389 |
* |
| 390 |
* @param int $user_id User ID. |
| 391 |
* |
| 392 |
* @return string Unique token. |
| 393 |
*/ |
| 394 |
public function get_token( int $user_id ): string { |
| 395 |
$token = bin2hex( Crypt::random_bytes( 32 ) ); |
| 396 |
update_user_meta( $user_id, Two_Fa_Component::TOKEN_USER_KEY, wp_hash( $user_id . $token ) ); |
| 397 |
|
| 398 |
return $token; |
| 399 |
} |
| 400 |
|
| 401 |
/** |
| 402 |
* Render otp form. Required conditions for the current user: |
| 403 |
* - is not logged in, |
| 404 |
* - user data is not empty, |
| 405 |
* - password matches the user, |
| 406 |
* - user role is checked on 2FA settings, |
| 407 |
* - user has at least one 2FA auth method available. |
| 408 |
* |
| 409 |
* @param null|WP_User|WP_Error $user Object of the logged-in user. |
| 410 |
* @param string $username Username or email address. |
| 411 |
* @param string $password Plain password string. |
| 412 |
*/ |
| 413 |
public function maybe_show_otp_form( $user, string $username, string $password ) { |
| 414 |
if ( |
| 415 |
! is_user_logged_in() |
| 416 |
&& '' !== trim( $password ) && $user instanceof WP_User |
| 417 |
&& wp_check_password( $password, $user->data->user_pass, $user->ID ) |
| 418 |
&& $this->service->is_auth_enable_for( $user, $this->model->user_roles ) |
| 419 |
&& array() !== $this->service->get_available_providers_for_user( $user ) |
| 420 |
) { |
| 421 |
$params = array(); |
| 422 |
$cookie = Array_Cache::get( 'auth_cookie', 'two_fa' ); |
| 423 |
if ( null !== $cookie ) { |
| 424 |
// Clear all session data if any. |
| 425 |
$session = WP_Session_Tokens::get_instance( $user->ID ); |
| 426 |
$session->destroy( $cookie['token'] ); |
| 427 |
} |
| 428 |
// Prevent user to login, and show otp screen. |
| 429 |
wp_clear_auth_cookie(); |
| 430 |
// All goods, we'll need to create a unique token to mark this user. |
| 431 |
$params['token'] = $this->get_token( $user->ID ); |
| 432 |
$params['password'] = $password; |
| 433 |
$params['user_id'] = $user->ID; |
| 434 |
// Get default provider. |
| 435 |
$params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID ); |
| 436 |
if ( Fallback_Email::$slug === $params['default_slug'] ) { |
| 437 |
$user_id = isset( $user->ID ) ? $user->ID : 0; |
| 438 |
$user_id = is_int( $user_id ) ? $user_id : (int) $user_id; |
| 439 |
$result = $this->service->send_otp_to_email( $params['token'], $user_id ); |
| 440 |
if ( is_wp_error( $result ) ) { |
| 441 |
$params['error'] = $result; |
| 442 |
$this->render_otp_screen( $params ); |
| 443 |
} |
| 444 |
} |
| 445 |
$this->render_otp_screen( $params ); |
| 446 |
} |
| 447 |
|
| 448 |
return $user; |
| 449 |
} |
| 450 |
|
| 451 |
/** |
| 452 |
* Render the OTP screen after login successful. |
| 453 |
* |
| 454 |
* @param array $params Additional parameters. |
| 455 |
* |
| 456 |
* @return void|null |
| 457 |
*/ |
| 458 |
private function render_otp_screen( array $params = array() ) { |
| 459 |
// Add common styles and scripts to enqueue. |
| 460 |
wp_enqueue_script( 'jquery' ); |
| 461 |
wp_enqueue_style( 'defender-otp-screen', defender_asset_url( '/assets/css/otp.css' ), array(), DEFENDER_VERSION ); |
| 462 |
|
| 463 |
$params['redirect_to'] = $this->redirect_url(); |
| 464 |
if ( ! isset( $params['error'] ) ) { |
| 465 |
$params['error'] = null; |
| 466 |
} |
| 467 |
|
| 468 |
$this->attach_behavior( WPMUDEV::class, WPMUDEV::class ); |
| 469 |
$params['custom_graphic'] = ''; |
| 470 |
$params['custom_graphic_type'] = $this->model->custom_graphic_type; |
| 471 |
$custom_graphic = ''; |
| 472 |
$custom_graphic_type = $this->model->custom_graphic_type; |
| 473 |
$custom_graphic_url = trim( $this->model->custom_graphic_url ); |
| 474 |
$custom_graphic_link = trim( $this->model->custom_graphic_link ); |
| 475 |
if ( $this->model->custom_graphic ) { |
| 476 |
if ( Two_Fa::CUSTOM_GRAPHIC_TYPE_UPLOAD === $custom_graphic_type && '' !== $custom_graphic_url ) { |
| 477 |
$custom_graphic = $custom_graphic_url; |
| 478 |
} elseif ( Two_Fa::CUSTOM_GRAPHIC_TYPE_LINK === $custom_graphic_type && '' !== $custom_graphic_link ) { |
| 479 |
$custom_graphic = $custom_graphic_link; |
| 480 |
} |
| 481 |
} |
| 482 |
|
| 483 |
$params['custom_graphic'] = $custom_graphic; |
| 484 |
$params['custom_graphic_type'] = $custom_graphic_type; |
| 485 |
|
| 486 |
$collection = $this->dump_routes_and_nonces(); |
| 487 |
$routes = $collection['routes']; |
| 488 |
$nonces = $collection['nonces']; |
| 489 |
|
| 490 |
$params['providers'] = array(); |
| 491 |
$user = null; |
| 492 |
if ( isset( $params['user_id'] ) ) { |
| 493 |
$user = get_user_by( 'id', $params['user_id'] ); |
| 494 |
if ( is_object( $user ) ) { |
| 495 |
$params['providers'] = $this->service->get_available_providers_for_user( $user ); |
| 496 |
// Get default provider. |
| 497 |
if ( ! isset( $params['default_slug'] ) || ! is_string( $params['default_slug'] ) || '' === trim( $params['default_slug'] ) ) { |
| 498 |
$params['default_slug'] = $this->service->get_default_provider_slug_for_user( $user->ID ); |
| 499 |
} |
| 500 |
} |
| 501 |
} |
| 502 |
|
| 503 |
$this->service->remove_actions_for_2fa_screen(); |
| 504 |
|
| 505 |
if ( |
| 506 |
isset( $params['providers'][ Webauthn::$slug ] ) && |
| 507 |
false === $params['providers'][ Webauthn::$slug ]->is_otp_screen_available( $user ) |
| 508 |
) { |
| 509 |
unset( $params['providers'][ Webauthn::$slug ] ); |
| 510 |
$params['default_slug'] = Webauthn::$slug !== $params['default_slug'] ? $params['default_slug'] : null; |
| 511 |
} |
| 512 |
|
| 513 |
if ( 0 === count( $params['providers'] ) ) { |
| 514 |
// Since 3.5.0. |
| 515 |
$error_msg = esc_html__( 'No providers.', 'defender-security' ); |
| 516 |
$params['error'] = new WP_Error( 'opt_fail', $error_msg ); |
| 517 |
do_action( 'wd_2fa_otp_params', $params ); |
| 518 |
|
| 519 |
wp_die( esc_html( $error_msg ) ); |
| 520 |
} |
| 521 |
// Add WebAuthn styles and scripts to enqueue. |
| 522 |
if ( true === array_key_exists( Webauthn::$slug, $params['providers'] ) ) { |
| 523 |
wp_enqueue_style( |
| 524 |
'defender-biometric-login-screen', |
| 525 |
defender_asset_url( '/assets/css/biometric.css' ), |
| 526 |
array(), |
| 527 |
DEFENDER_VERSION |
| 528 |
); |
| 529 |
wp_enqueue_script( |
| 530 |
'wpdef_webauthn_common_script', |
| 531 |
plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ), |
| 532 |
array(), |
| 533 |
DEFENDER_VERSION, |
| 534 |
true |
| 535 |
); |
| 536 |
wp_enqueue_script( |
| 537 |
'defender-biometric-login-script', |
| 538 |
plugins_url( 'assets/js/biometric-login.js', WP_DEFENDER_FILE ), |
| 539 |
array( |
| 540 |
'jquery', |
| 541 |
'wpdef_webauthn_common_script', |
| 542 |
), |
| 543 |
DEFENDER_VERSION, |
| 544 |
true |
| 545 |
); |
| 546 |
$webauthn_controller = wd_di()->get( Webauthn_Controller::class ); |
| 547 |
wp_localize_script( |
| 548 |
'defender-biometric-login-script', |
| 549 |
'webauthn', |
| 550 |
array( |
| 551 |
'admin_url' => admin_url( 'admin-ajax.php' ), |
| 552 |
'nonce' => wp_create_nonce( 'wpdef_webauthn' ), |
| 553 |
'i18n' => $webauthn_controller->get_translations(), |
| 554 |
'username' => isset( $user->user_login ) && is_string( $user->user_login ) && '' !== trim( $user->user_login ) ? $user->user_login : '', |
| 555 |
'provider_slug' => Webauthn::$slug, |
| 556 |
) |
| 557 |
); |
| 558 |
} |
| 559 |
// Prepare data. |
| 560 |
$args = array( |
| 561 |
'action' => defender_base_action(), |
| 562 |
'_def_nonce' => $nonces['send_backup_code'], |
| 563 |
// Add a dummy values to avoid displaying errors, e.g. for the case with null. |
| 564 |
'route' => $this->check_route( $routes['send_backup_code'] ?? 'test' ), |
| 565 |
); |
| 566 |
// If user's session has expired add a new 'interimlogin'-arg. |
| 567 |
$interim_login = defender_get_data_from_request( 'interim-login', 'r' ); |
| 568 |
if ( is_string( $interim_login ) && '' !== trim( $interim_login ) ) { |
| 569 |
$args['interimlogin'] = 'yes'; |
| 570 |
} |
| 571 |
$params['action_fallback_email'] = add_query_arg( $args, admin_url( 'admin-ajax.php' ) ); |
| 572 |
|
| 573 |
// Since 3.5.0. |
| 574 |
do_action( 'wd_2fa_otp_params', $params ); |
| 575 |
|
| 576 |
$this->render_partial( 'two-fa/otp', $params ); |
| 577 |
exit; |
| 578 |
} |
| 579 |
|
| 580 |
/** |
| 581 |
* Store the session key in the array cache for two-factor authentication. |
| 582 |
* |
| 583 |
* @param string $cookie The authentication cookie. |
| 584 |
* |
| 585 |
* @return void |
| 586 |
*/ |
| 587 |
public function store_session_key( $cookie ): void { |
| 588 |
// Clear login cookie to ensure nonce consistency. |
| 589 |
if ( ! is_user_logged_in() && isset( $_COOKIE[ LOGGED_IN_COOKIE ] ) ) { |
| 590 |
unset( $_COOKIE[ LOGGED_IN_COOKIE ] ); |
| 591 |
} |
| 592 |
|
| 593 |
$cookie = wp_parse_auth_cookie( $cookie, 'logged_in' ); |
| 594 |
Array_Cache::set( 'auth_cookie', $cookie, 'two_fa' ); |
| 595 |
} |
| 596 |
|
| 597 |
/** |
| 598 |
* Disable 2FA TOTP method for the current user. It's not from the list of routes. |
| 599 |
* |
| 600 |
* @return Response |
| 601 |
* @defender_route |
| 602 |
* @is_public |
| 603 |
*/ |
| 604 |
public function disable_totp(): Response { |
| 605 |
$user_id = get_current_user_id(); |
| 606 |
// Remove TOTP flag. |
| 607 |
delete_user_meta( $user_id, Totp::TOTP_AUTH_KEY ); |
| 608 |
// Remove old secret key. |
| 609 |
delete_user_meta( $user_id, Totp::TOTP_SECRET_KEY ); |
| 610 |
// Remove new secret key. |
| 611 |
delete_user_meta( $user_id, Totp::TOTP_SODIUM_SECRET_KEY ); |
| 612 |
// Remove TOTP from enabled providers. |
| 613 |
$enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true ); |
| 614 |
if ( is_array( $enabled_providers ) && array() !== $enabled_providers ) { |
| 615 |
foreach ( $enabled_providers as $key => $slug ) { |
| 616 |
if ( Totp::$slug === $slug ) { |
| 617 |
unset( $enabled_providers[ $key ] ); |
| 618 |
break; |
| 619 |
} |
| 620 |
} |
| 621 |
} else { |
| 622 |
$enabled_providers = ''; |
| 623 |
} |
| 624 |
update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers ); |
| 625 |
// Check the default provider. If it's TOTP then clear the value. |
| 626 |
$default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true ); |
| 627 |
if ( is_string( $default_provider ) && '' !== trim( $default_provider ) && Totp::$slug === $default_provider ) { |
| 628 |
update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' ); |
| 629 |
} |
| 630 |
|
| 631 |
return new Response( true, array() ); |
| 632 |
} |
| 633 |
|
| 634 |
/** |
| 635 |
* Verify the OTP and enable 2-factor authentication for the currently logged in user. |
| 636 |
* |
| 637 |
* @param Request $request The request object containing the OTP and setup key. |
| 638 |
* |
| 639 |
* @return Response A response object indicating the success or failure of the operation. |
| 640 |
* If successful, the response does not contain any data. |
| 641 |
* If failed, the response contains an error message. |
| 642 |
* @defender_route |
| 643 |
* @is_public |
| 644 |
* @throws SodiumException Exceptions thrown by the sodium functions. |
| 645 |
*/ |
| 646 |
public function verify_otp_for_enabling( Request $request ): Response { |
| 647 |
if ( is_user_logged_in() ) { |
| 648 |
$data = $request->get_data(); |
| 649 |
$otp = isset( $data['otp'] ) ? sanitize_text_field( $data['otp'] ) : false; |
| 650 |
if ( false === $otp || strlen( $otp ) < 6 ) { |
| 651 |
return new Response( |
| 652 |
false, |
| 653 |
array( 'message' => esc_html__( 'Please input a valid OTP code.', 'defender-security' ) ) |
| 654 |
); |
| 655 |
} |
| 656 |
// Get the setup key. |
| 657 |
$setup_key = $data['setup_key'] ?? false; |
| 658 |
if ( ! $setup_key ) { |
| 659 |
return new Response( |
| 660 |
false, |
| 661 |
array( 'message' => esc_html__( 'The setup key is incorrect.', 'defender-security' ) ) |
| 662 |
); |
| 663 |
} |
| 664 |
$user_id = get_current_user_id(); |
| 665 |
$result = TOTP::verify_otp( $otp, $user_id, $setup_key ); |
| 666 |
// OTP result can be a boolean value or WP error. |
| 667 |
if ( is_wp_error( $result ) ) { |
| 668 |
return new Response( |
| 669 |
false, |
| 670 |
array( 'message' => $result->get_error_message() ) |
| 671 |
); |
| 672 |
} |
| 673 |
if ( $result ) { |
| 674 |
// Save a setup key. |
| 675 |
$result = Totp::save_setup_key( $user_id, $setup_key ); |
| 676 |
if ( is_wp_error( $result ) ) { |
| 677 |
return new Response( |
| 678 |
false, |
| 679 |
array( 'message' => $result->get_error_message() ) |
| 680 |
); |
| 681 |
} |
| 682 |
// Enable OTP. |
| 683 |
$this->service->enable_otp( $user_id ); |
| 684 |
$totp_slug = Totp::$slug; |
| 685 |
// Add TOTP to enabled providers. |
| 686 |
$enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true ); |
| 687 |
if ( is_array( $enabled_providers ) && array() !== $enabled_providers ) { |
| 688 |
// Array of enabled providers is not empty now. |
| 689 |
if ( ! in_array( Totp::$slug, $enabled_providers, true ) ) { |
| 690 |
$enabled_providers[] = $totp_slug; |
| 691 |
update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers ); |
| 692 |
} |
| 693 |
} else { |
| 694 |
// Array of enabled providers is empty now. |
| 695 |
update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, array( $totp_slug ) ); |
| 696 |
} |
| 697 |
// If no default provider then add TOTP as it. |
| 698 |
$default_provider = get_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, true ); |
| 699 |
if ( ! is_string( $default_provider ) || '' === trim( $default_provider ) ) { |
| 700 |
update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $totp_slug ); |
| 701 |
} |
| 702 |
|
| 703 |
return new Response( true, array() ); |
| 704 |
} else { |
| 705 |
return new Response( |
| 706 |
false, |
| 707 |
array( 'message' => esc_html__( 'Your OTP code is incorrect. Please try again.', 'defender-security' ) ) |
| 708 |
); |
| 709 |
} |
| 710 |
} |
| 711 |
return new Response( true, array() ); |
| 712 |
} |
| 713 |
|
| 714 |
/** |
| 715 |
* Clear 2FA providers for the given user. |
| 716 |
* |
| 717 |
* @param int $user_id User ID. |
| 718 |
*/ |
| 719 |
protected function clear_providers( int $user_id ): void { |
| 720 |
update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, '' ); |
| 721 |
update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, '' ); |
| 722 |
} |
| 723 |
|
| 724 |
/** |
| 725 |
* Updates the user's 2FA profile with the selected providers and default provider. |
| 726 |
* |
| 727 |
* @param int $user_id The ID of the user whose profile is being updated. |
| 728 |
* |
| 729 |
* @return void |
| 730 |
*/ |
| 731 |
public function profile_update( int $user_id ) { |
| 732 |
$post_data = defender_get_data_from_request( null, 'p' ); |
| 733 |
if ( isset( $post_data['_wpdef_2fa_nonce_user_options'] ) ) { |
| 734 |
check_admin_referer( 'wpdef_2fa_user_options', '_wpdef_2fa_nonce_user_options' ); |
| 735 |
|
| 736 |
if ( |
| 737 |
! isset( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] ) |
| 738 |
|| ! is_array( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ] ) |
| 739 |
) { |
| 740 |
return; |
| 741 |
} |
| 742 |
// Remove empty elements. |
| 743 |
$checked_providers = array_diff( $post_data[ Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY ], array( '' ) ); |
| 744 |
// If no option is checked then the values for default provider and enabled providers are cleared. |
| 745 |
if ( array() === $checked_providers ) { |
| 746 |
$this->clear_providers( $user_id ); |
| 747 |
|
| 748 |
return; |
| 749 |
} |
| 750 |
|
| 751 |
$providers = $this->service->get_providers(); |
| 752 |
// For Fallback-Email method: the email value should be not empty and valid. |
| 753 |
if ( in_array( Fallback_Email::$slug, $checked_providers, true ) ) { |
| 754 |
$email = HTTP::post( 'def_2fa_backup_email' ); |
| 755 |
if ( is_string( $email ) && '' !== trim( $email ) && filter_var( $email, FILTER_VALIDATE_EMAIL ) ) { |
| 756 |
update_user_meta( $user_id, Fallback_Email::FALLBACK_EMAIL_KEY, $email ); |
| 757 |
} else { |
| 758 |
unset( $checked_providers[ Fallback_Email::$slug ] ); |
| 759 |
} |
| 760 |
} |
| 761 |
|
| 762 |
// For Webauthn method: a user must have at least once device registered. |
| 763 |
$key = array_search( Webauthn::$slug, $checked_providers, true ); |
| 764 |
if ( false !== $key ) { |
| 765 |
$user_authenticators = wd_di()->get( Webauthn_Controller::class )->get_current_user_authenticators(); |
| 766 |
if ( 0 === count( $user_authenticators ) ) { |
| 767 |
unset( $checked_providers[ $key ] ); |
| 768 |
} |
| 769 |
} |
| 770 |
// Case when WebAuthn is checked but no registered devices OR Fallback_Email has an invalid email value. |
| 771 |
if ( array() === $checked_providers ) { |
| 772 |
$this->clear_providers( $user_id ); |
| 773 |
|
| 774 |
return; |
| 775 |
} |
| 776 |
|
| 777 |
// Current user. |
| 778 |
$user = get_user_by( 'id', $user_id ); |
| 779 |
// Enable only the available providers. |
| 780 |
$enabled_providers = array(); |
| 781 |
foreach ( $providers as $slug => $provider ) { |
| 782 |
if ( in_array( $slug, $checked_providers, true ) && $provider->is_available_for_user( $user ) ) { |
| 783 |
$enabled_providers[] = $slug; |
| 784 |
} |
| 785 |
} |
| 786 |
update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers ); |
| 787 |
/** |
| 788 |
* Fires when 2fa providers are enabled. |
| 789 |
* |
| 790 |
* @since 4.3.0 |
| 791 |
*/ |
| 792 |
do_action( 'wd_2fa_enabled_provider_slugs', $enabled_providers ); |
| 793 |
// Default provider must be enabled. |
| 794 |
$default_provider = $post_data[ Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY ] ?? ''; |
| 795 |
// The case#1 when all 2fa providers were deactivated before. |
| 796 |
if ( ! is_string( $default_provider ) || '' === trim( $default_provider ) ) { |
| 797 |
$default_provider = $enabled_providers[0]; |
| 798 |
} |
| 799 |
// The case#2 when prev default provider is deactivated and another one is activated. |
| 800 |
if ( ! in_array( $default_provider, $checked_providers, true ) ) { |
| 801 |
$default_provider = $enabled_providers[0]; |
| 802 |
} |
| 803 |
// Save default provider. |
| 804 |
update_user_meta( $user_id, Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, $default_provider ); |
| 805 |
} |
| 806 |
} |
| 807 |
|
| 808 |
/** |
| 809 |
* A simple filter to show activate 2fa screen on profile page. |
| 810 |
* |
| 811 |
* @param WP_User $user The current WP_User object. |
| 812 |
* |
| 813 |
* @return void |
| 814 |
*/ |
| 815 |
public function show_user_profile( WP_User $user ): void { |
| 816 |
$user_roles = $this->get_roles( $user ); |
| 817 |
// This method is better than is_intersected_arrays() because it is flexibly controlled with a nested hook. |
| 818 |
if ( is_array( $user_roles ) && array() !== $user_roles && $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) { |
| 819 |
wp_enqueue_style( 'defender-profile-2fa', defender_asset_url( '/assets/css/two-factor.css' ), array(), DEFENDER_VERSION ); |
| 820 |
|
| 821 |
$webauthn_controller = wd_di()->get( Webauthn_Controller::class ); |
| 822 |
$webauthn_requirements = $this->check_webauthn_requirements(); |
| 823 |
if ( $this->service->is_checked_enabled_provider_by_slug( |
| 824 |
$user, |
| 825 |
Webauthn::$slug |
| 826 |
) && ! $webauthn_requirements ) { |
| 827 |
$this->service->remove_enabled_provider_for_user( Webauthn::$slug, $user ); |
| 828 |
} |
| 829 |
|
| 830 |
$webauthn_user_handle_match_failed = wd_di()->get( Webauthn_Component::class )->getUserHandleMatchFailed( $user->ID ); |
| 831 |
|
| 832 |
wp_enqueue_script( |
| 833 |
'wpdef_webauthn_common_script', |
| 834 |
plugins_url( 'assets/js/webauthn-common.js', WP_DEFENDER_FILE ), |
| 835 |
array(), |
| 836 |
DEFENDER_VERSION, |
| 837 |
true |
| 838 |
); |
| 839 |
wp_enqueue_script( |
| 840 |
'wpdef_webauthn_script', |
| 841 |
plugins_url( 'assets/js/webauthn.js', WP_DEFENDER_FILE ), |
| 842 |
array( |
| 843 |
'jquery', |
| 844 |
'wpdef_webauthn_common_script', |
| 845 |
'wp-i18n', |
| 846 |
), |
| 847 |
DEFENDER_VERSION, |
| 848 |
true |
| 849 |
); |
| 850 |
wp_localize_script( |
| 851 |
'wpdef_webauthn_script', |
| 852 |
'webauthn', |
| 853 |
array( |
| 854 |
'admin_url' => admin_url( 'admin-ajax.php' ), |
| 855 |
'nonce' => wp_create_nonce( 'wpdef_webauthn' ), |
| 856 |
'i18n' => $webauthn_controller->get_translations(), |
| 857 |
'registered_auths' => $webauthn_controller->get_current_user_authenticators(), |
| 858 |
'username' => isset( $user->user_login ) && is_string( $user->user_login ) ? $user->user_login : '', |
| 859 |
'user_handle_match_failed' => $webauthn_user_handle_match_failed, |
| 860 |
) |
| 861 |
); |
| 862 |
|
| 863 |
$forced_auth = $this->service->is_intersected_arrays( |
| 864 |
$user_roles, |
| 865 |
$this->model->force_auth_roles |
| 866 |
); |
| 867 |
$default_values = $this->model->get_default_values(); |
| 868 |
$enabled_providers = $this->service->get_available_providers_for_user( $user ); |
| 869 |
$enabled_provider_slugs = array() !== $enabled_providers ? array_keys( $enabled_providers ) : array(); |
| 870 |
$default_provider_slug = $this->service->get_default_provider_slug_for_user( $user->ID ); |
| 871 |
$webauthn_enabled = $this->service->is_checked_enabled_provider_by_slug( $user, Webauthn::$slug ); |
| 872 |
|
| 873 |
$this->render_partial( |
| 874 |
'two-fa/user-options', |
| 875 |
array( |
| 876 |
'is_force_auth' => $forced_auth && $this->model->force_auth && array() === $enabled_providers, |
| 877 |
'force_auth_message' => $this->model->force_auth_mess, |
| 878 |
'default_message' => $default_values['message'], |
| 879 |
'user' => $user, |
| 880 |
'all_providers' => $this->service->get_providers(), |
| 881 |
'enabled_providers_key' => Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, |
| 882 |
'default_provider_key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, |
| 883 |
'checked_provider_slugs' => $enabled_provider_slugs, |
| 884 |
'checked_def_provider_slug' => is_string( $default_provider_slug ) && '' !== trim( $default_provider_slug ) ? $default_provider_slug : null, |
| 885 |
'webauthn_requirements' => $webauthn_requirements, |
| 886 |
'webauthn_enabled' => $webauthn_enabled, |
| 887 |
'webauthn_slug' => Webauthn::$slug, |
| 888 |
'is_admin' => is_admin(), |
| 889 |
) |
| 890 |
); |
| 891 |
} |
| 892 |
} |
| 893 |
|
| 894 |
/** |
| 895 |
* Save settings. |
| 896 |
* |
| 897 |
* @param Request $request The request object containing new settings data. |
| 898 |
* |
| 899 |
* @return Response |
| 900 |
* @defender_route |
| 901 |
*/ |
| 902 |
public function save_settings( Request $request ): Response { |
| 903 |
$model = $this->model; |
| 904 |
$data = $request->get_data(); |
| 905 |
$old_detect_woo = $model->detect_woo; |
| 906 |
|
| 907 |
$model->import( $data ); |
| 908 |
if ( $model->validate() ) { |
| 909 |
$model->save(); |
| 910 |
|
| 911 |
if ( $old_detect_woo !== $model->detect_woo ) { |
| 912 |
if ( $model->detect_woo ) { |
| 913 |
$this->wp_defender_2fa_endpoint(); |
| 914 |
} |
| 915 |
|
| 916 |
flush_rewrite_rules(); |
| 917 |
} |
| 918 |
|
| 919 |
Config_Hub_Helper::set_clear_active_flag(); |
| 920 |
|
| 921 |
return new Response( |
| 922 |
true, |
| 923 |
array_merge( |
| 924 |
array( |
| 925 |
'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ), |
| 926 |
'auto_close' => true, |
| 927 |
), |
| 928 |
$this->data_frontend() |
| 929 |
) |
| 930 |
); |
| 931 |
} |
| 932 |
|
| 933 |
return new Response( |
| 934 |
false, |
| 935 |
array( 'message' => $model->get_formatted_errors() ) |
| 936 |
); |
| 937 |
} |
| 938 |
|
| 939 |
/** |
| 940 |
* Send test email, use in settings screen. |
| 941 |
* |
| 942 |
* @param Request $request Request object. |
| 943 |
* |
| 944 |
* @return Response |
| 945 |
* @defender_route |
| 946 |
*/ |
| 947 |
public function send_test_email( Request $request ): Response { |
| 948 |
$data = $request->get_data( |
| 949 |
array( |
| 950 |
'email_subject' => array( |
| 951 |
'type' => 'string', |
| 952 |
'sanitize' => 'sanitize_text_field', |
| 953 |
), |
| 954 |
'email_sender' => array( |
| 955 |
'type' => 'string', |
| 956 |
'sanitize' => 'sanitize_text_field', |
| 957 |
), |
| 958 |
'email_body' => array( |
| 959 |
'type' => 'string', |
| 960 |
'sanitize' => 'wp_kses_post', |
| 961 |
), |
| 962 |
) |
| 963 |
); |
| 964 |
|
| 965 |
$subject = $data['email_subject']; |
| 966 |
$sender = $data['email_sender']; |
| 967 |
$body = $this->render_partial( |
| 968 |
'email/2fa-lost-phone', |
| 969 |
array( |
| 970 |
'body' => $data['email_body'], |
| 971 |
), |
| 972 |
false |
| 973 |
); |
| 974 |
|
| 975 |
$params = array( |
| 976 |
'passcode' => '[a-sample-passcode]', |
| 977 |
'display_name' => $this->get_user_display( get_current_user_id() ), |
| 978 |
); |
| 979 |
|
| 980 |
foreach ( $params as $key => $param ) { |
| 981 |
if ( 'passcode' === $key ) { |
| 982 |
$body = str_replace( "{{{$key}}}", '<span class="defender-otp">' . $param . '</span>', $body ); |
| 983 |
} else { |
| 984 |
$body = str_replace( "{{{$key}}}", $param, $body ); |
| 985 |
} |
| 986 |
} |
| 987 |
$headers = array( 'Content-Type: text/html; charset=UTF-8' ); |
| 988 |
if ( is_string( $sender ) && '' !== trim( $sender ) ) { |
| 989 |
// Since v5.2.0. |
| 990 |
$from_email = defender_noreply_email( 'wd_two_fa_totp_noreply_email' ); |
| 991 |
$headers[] = sprintf( 'From: %s <%s>', $sender, $from_email ); |
| 992 |
} else { |
| 993 |
return new Response( |
| 994 |
false, |
| 995 |
array( 'message' => esc_html__( 'Sender value cannot be empty.', 'defender-security' ) ) |
| 996 |
); |
| 997 |
} |
| 998 |
// Main email template. |
| 999 |
$body = $this->render_partial( |
| 1000 |
'email/index', |
| 1001 |
array( |
| 1002 |
'title' => Two_Fa::get_module_name(), |
| 1003 |
'content_body' => $body, |
| 1004 |
// An empty value because 2FA-email is sent after a manual click from the user. |
| 1005 |
'unsubscribe_link' => '', |
| 1006 |
), |
| 1007 |
false |
| 1008 |
); |
| 1009 |
|
| 1010 |
$send_mail = wp_mail( Fallback_Email::get_backup_email(), $subject, $body, $headers ); |
| 1011 |
if ( $send_mail ) { |
| 1012 |
return new Response( |
| 1013 |
true, |
| 1014 |
array( 'message' => esc_html__( 'Test email has been sent to your email.', 'defender-security' ) ) |
| 1015 |
); |
| 1016 |
} else { |
| 1017 |
return new Response( |
| 1018 |
false, |
| 1019 |
array( 'message' => esc_html__( 'Test email failed.', 'defender-security' ) ) |
| 1020 |
); |
| 1021 |
} |
| 1022 |
} |
| 1023 |
|
| 1024 |
/** |
| 1025 |
* Converts the current object state to an array. |
| 1026 |
* |
| 1027 |
* @return array The array representation of the object. |
| 1028 |
*/ |
| 1029 |
public function to_array(): array { |
| 1030 |
$settings = new Two_Fa(); |
| 1031 |
[ $routes, $nonces ] = Route::export_routes( 'two_fa' ); |
| 1032 |
|
| 1033 |
return array( |
| 1034 |
'enabled' => $settings->enabled, |
| 1035 |
'useable' => $settings->enabled && count( $settings->user_roles ), |
| 1036 |
'nonces' => $nonces, |
| 1037 |
'endpoints' => $routes, |
| 1038 |
); |
| 1039 |
} |
| 1040 |
|
| 1041 |
/** |
| 1042 |
* Removes settings for all submodules. |
| 1043 |
*/ |
| 1044 |
public function remove_settings(): void { |
| 1045 |
( new Two_Fa() )->delete(); |
| 1046 |
} |
| 1047 |
|
| 1048 |
/** |
| 1049 |
* Delete all the data & the cache. |
| 1050 |
*/ |
| 1051 |
public function remove_data(): void { |
| 1052 |
global $wpdb; |
| 1053 |
|
| 1054 |
$keys = array( |
| 1055 |
Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, |
| 1056 |
Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, |
| 1057 |
// From Totp. |
| 1058 |
'wd_2fa_attempt_' . TOTP::$slug, |
| 1059 |
TOTP::TOTP_AUTH_KEY, |
| 1060 |
// For backward compatible with the def.key file. We'll remove this key in future versions and use the key for Sodium. |
| 1061 |
TOTP::TOTP_SECRET_KEY, |
| 1062 |
TOTP::TOTP_SODIUM_SECRET_KEY, |
| 1063 |
TOTP::TOTP_FORCE_KEY, |
| 1064 |
// From Backup_Codes. |
| 1065 |
'wd_2fa_attempt_' . Backup_Codes::$slug, |
| 1066 |
Backup_Codes::BACKUP_CODE_START, |
| 1067 |
Backup_Codes::BACKUP_CODE_VALUES, |
| 1068 |
// From Fallback_Email. |
| 1069 |
'wd_2fa_attempt_' . Fallback_Email::$slug, |
| 1070 |
Fallback_Email::FALLBACK_EMAIL_KEY, |
| 1071 |
Fallback_Email::FALLBACK_BACKUP_CODE_KEY, |
| 1072 |
); |
| 1073 |
$sql = "DELETE FROM {$wpdb->usermeta} WHERE meta_key IN (" . implode( |
| 1074 |
',', |
| 1075 |
array_fill( 0, count( $keys ), '%s' ) |
| 1076 |
) . ');'; |
| 1077 |
$query = call_user_func_array( array( $wpdb, 'prepare' ), array_merge( array( $sql ), $keys ) ); |
| 1078 |
$wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery |
| 1079 |
// From Webauthn. |
| 1080 |
wd_di()->get( Webauthn_Controller::class )->remove_data(); |
| 1081 |
// Check if 2fa file exists. |
| 1082 |
$file = $this->get_2fa_lock_path(); |
| 1083 |
if ( is_file( $file ) && is_readable( $file ) ) { |
| 1084 |
// Delete 2fa file. It's actual for prev v3.3.1. |
| 1085 |
wp_delete_file( $file ); |
| 1086 |
} |
| 1087 |
// Check if the file with a random key exists. |
| 1088 |
$file = Crypt::get_path_to_key_file(); |
| 1089 |
if ( is_file( $file ) && is_readable( $file ) ) { |
| 1090 |
wp_delete_file( $file ); |
| 1091 |
} |
| 1092 |
// Remove cached data. |
| 1093 |
Array_Cache::remove( 'auth_cookie', 'two_fa' ); |
| 1094 |
Array_Cache::remove( 'providers', 'two_fa' ); |
| 1095 |
} |
| 1096 |
|
| 1097 |
/** |
| 1098 |
* Filters users by 2FA option. |
| 1099 |
* |
| 1100 |
* @param WP_User_Query $query The user query object. |
| 1101 |
* |
| 1102 |
* @return void |
| 1103 |
*/ |
| 1104 |
public function filter_users_by_2fa( $query ): void { |
| 1105 |
global $pagenow; |
| 1106 |
|
| 1107 |
$is_enabled = defender_get_data_from_request( 'wpdef_two_fa', 'g' ); |
| 1108 |
if ( is_admin() && 'users.php' === $pagenow && 'enabled' === $is_enabled ) { |
| 1109 |
$query->set( |
| 1110 |
'meta_query', |
| 1111 |
array( |
| 1112 |
array( |
| 1113 |
'key' => Two_Fa_Component::DEFAULT_PROVIDER_USER_KEY, |
| 1114 |
'value' => array_keys( $this->service->get_providers() ), |
| 1115 |
'compare' => 'IN', |
| 1116 |
), |
| 1117 |
) |
| 1118 |
); |
| 1119 |
} |
| 1120 |
} |
| 1121 |
|
| 1122 |
/** |
| 1123 |
* Provides data for the frontend. |
| 1124 |
* |
| 1125 |
* @return array An array of data for the frontend. |
| 1126 |
*/ |
| 1127 |
public function data_frontend(): array { |
| 1128 |
return array_merge( |
| 1129 |
array( |
| 1130 |
'model' => $this->model->export(), |
| 1131 |
'defaults' => $this->model->get_default_values(), |
| 1132 |
'all_roles' => $this->get_all_editable_roles(), |
| 1133 |
'count' => $this->service->count_users_with_enabled_2fa(), |
| 1134 |
'notices' => $this->compatibility_notices, |
| 1135 |
'count_checked_roles' => count( $this->model->user_roles ), |
| 1136 |
'is_woo_active' => $this->is_woo_activated, |
| 1137 |
// The multisite check is an isolated case now. If it will be needed for several modules, then a more global scope is needed. |
| 1138 |
'is_multisite' => is_multisite(), |
| 1139 |
'module_name' => Two_Fa::get_module_name(), |
| 1140 |
'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(), |
| 1141 |
'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(), |
| 1142 |
), |
| 1143 |
$this->dump_routes_and_nonces() |
| 1144 |
); |
| 1145 |
} |
| 1146 |
|
| 1147 |
/** |
| 1148 |
* Imports data into the model. |
| 1149 |
* |
| 1150 |
* @param array $data Data to be imported into the model. |
| 1151 |
* |
| 1152 |
* @throws Exception If table is not defined. |
| 1153 |
*/ |
| 1154 |
public function import_data( array $data ) { |
| 1155 |
$model = new Two_Fa(); |
| 1156 |
|
| 1157 |
$model->import( $data ); |
| 1158 |
/** |
| 1159 |
* Sometime, the custom image broken on import. When that happen, we will revert to the default image. |
| 1160 |
*/ |
| 1161 |
$model->custom_graphic_url = $this->service->get_custom_graphic_url( $model->custom_graphic_url ); |
| 1162 |
if ( $model->validate() ) { |
| 1163 |
$model->save(); |
| 1164 |
} |
| 1165 |
} |
| 1166 |
|
| 1167 |
/** |
| 1168 |
* Exports strings. |
| 1169 |
* |
| 1170 |
* @return array An array of strings. |
| 1171 |
*/ |
| 1172 |
public function export_strings(): array { |
| 1173 |
$settings = new Two_Fa(); |
| 1174 |
|
| 1175 |
return array( |
| 1176 |
$settings->enabled ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ), |
| 1177 |
); |
| 1178 |
} |
| 1179 |
|
| 1180 |
/** |
| 1181 |
* Generates configuration strings based on the provided configuration. |
| 1182 |
* |
| 1183 |
* @param array $config Configuration data. |
| 1184 |
* |
| 1185 |
* @return array Returns an array of configuration strings. |
| 1186 |
*/ |
| 1187 |
public function config_strings( array $config ): array { |
| 1188 |
return array( |
| 1189 |
$config['enabled'] ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ), |
| 1190 |
); |
| 1191 |
} |
| 1192 |
|
| 1193 |
/** |
| 1194 |
* WooCommerce prevents any user who cannot 'edit_posts' (subscribers, customers etc.) from accessing admin. |
| 1195 |
* Here we are disabling WooCommerce default behavior, if force 2FA is enabled. |
| 1196 |
* |
| 1197 |
* @param bool $prevent Prevent admin access. |
| 1198 |
* |
| 1199 |
* @return bool|null |
| 1200 |
*/ |
| 1201 |
public function handle_woocommerce_prevent_admin_access( bool $prevent ) { |
| 1202 |
$user = $this->current_user; |
| 1203 |
if ( ! is_object( $user ) ) { |
| 1204 |
return; |
| 1205 |
} |
| 1206 |
// Is User role from common list checked? |
| 1207 |
if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) { |
| 1208 |
return $prevent; |
| 1209 |
} |
| 1210 |
// Is 'Force Authentication' checked? |
| 1211 |
if ( false === $this->model->force_auth ) { |
| 1212 |
return $prevent; |
| 1213 |
} |
| 1214 |
// Is User role from forced list checked? |
| 1215 |
if ( $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) { |
| 1216 |
return false; |
| 1217 |
} |
| 1218 |
// Is TOTP saved with a passcode? |
| 1219 |
if ( array() !== $this->service->get_available_providers_for_user( $user ) ) { |
| 1220 |
return $prevent; |
| 1221 |
} |
| 1222 |
|
| 1223 |
return $prevent; |
| 1224 |
} |
| 1225 |
|
| 1226 |
/** |
| 1227 |
* WooCommerce specific hooks. |
| 1228 |
* |
| 1229 |
* @return void |
| 1230 |
*/ |
| 1231 |
private function woocommerce_hooks(): void { |
| 1232 |
// This filter added only for disable WooCommerce default behavior. |
| 1233 |
add_filter( |
| 1234 |
'woocommerce_prevent_admin_access', |
| 1235 |
array( |
| 1236 |
$this, |
| 1237 |
'handle_woocommerce_prevent_admin_access', |
| 1238 |
), |
| 1239 |
10, |
| 1240 |
1 |
| 1241 |
); |
| 1242 |
// Handle WooCommerce MyAccount page login redirect. |
| 1243 |
add_filter( 'woocommerce_login_redirect', array( $this, 'handle_woocommerce_login_redirect' ), 10, 2 ); |
| 1244 |
// Add field. |
| 1245 |
add_action( 'woocommerce_login_form_end', array( $this, 'add_redirect_to_input' ) ); |
| 1246 |
} |
| 1247 |
|
| 1248 |
/** |
| 1249 |
* WooCommerce by default redirect users to My-account page. |
| 1250 |
* Here we are checking force 2FA is enabled or not. |
| 1251 |
* |
| 1252 |
* @param string $redirect Redirect URL. |
| 1253 |
* @param WP_User $user Logged-in user. |
| 1254 |
* |
| 1255 |
* @return string |
| 1256 |
*/ |
| 1257 |
public function handle_woocommerce_login_redirect( string $redirect, WP_User $user ) { |
| 1258 |
// Is User role from common list checked? |
| 1259 |
if ( false === $this->service->is_auth_enable_for( $user, $this->model->user_roles ) ) { |
| 1260 |
return $redirect; |
| 1261 |
} |
| 1262 |
// Is 'Force Authentication' checked? |
| 1263 |
if ( false === $this->model->force_auth ) { |
| 1264 |
return $redirect; |
| 1265 |
} |
| 1266 |
// Is User role from forced list checked? |
| 1267 |
if ( ! $this->service->is_force_auth_enable_for( $user->ID, $this->model->force_auth_roles ) ) { |
| 1268 |
return $redirect; |
| 1269 |
} |
| 1270 |
// Is TOTP saved with a passcode? |
| 1271 |
if ( array() === $this->service->get_available_providers_for_user( $user ) ) { |
| 1272 |
return admin_url( 'profile.php' ) . '#defender-security'; |
| 1273 |
} |
| 1274 |
|
| 1275 |
return $redirect; |
| 1276 |
} |
| 1277 |
|
| 1278 |
/** |
| 1279 |
* Return redirect URL after 2FA submit. |
| 1280 |
*/ |
| 1281 |
private function redirect_url() { |
| 1282 |
return HTTP::post( 'redirect_to', defender_get_request_url() ); |
| 1283 |
} |
| 1284 |
|
| 1285 |
/** |
| 1286 |
* Adds redirect_to hidden input to Woo login. |
| 1287 |
* |
| 1288 |
* @return void |
| 1289 |
*/ |
| 1290 |
public function add_redirect_to_input(): void { |
| 1291 |
echo '<input type="hidden" name="redirect_to" value="' . esc_url_raw( defender_get_request_url() ) . '">'; |
| 1292 |
} |
| 1293 |
|
| 1294 |
/** |
| 1295 |
* Generate Backup codes on Profile page. |
| 1296 |
* |
| 1297 |
* @return Response |
| 1298 |
* @defender_route |
| 1299 |
* @is_public |
| 1300 |
*/ |
| 1301 |
public function generate_backup_codes(): Response { |
| 1302 |
$user = wp_get_current_user(); |
| 1303 |
|
| 1304 |
return new Response( |
| 1305 |
true, |
| 1306 |
array( |
| 1307 |
'codes' => Backup_Codes::generate_codes( $user ), |
| 1308 |
'count' => Backup_Codes::display_number_of_codes( Backup_Codes::get_unused_codes_for_user( $user ) ), |
| 1309 |
'title' => sprintf( |
| 1310 |
/* translators: %s: count */ |
| 1311 |
esc_html__( '2FA Backup Codes for %s:', 'defender-security' ), |
| 1312 |
get_bloginfo( 'url' ) |
| 1313 |
), |
| 1314 |
'button_text' => esc_html__( 'Get New Codes', 'defender-security' ), |
| 1315 |
'description' => esc_html__( 'Each backup code can only be used to log in once.', 'defender-security' ), |
| 1316 |
) |
| 1317 |
); |
| 1318 |
} |
| 1319 |
|
| 1320 |
/** |
| 1321 |
* Shortcode to display 2FA user settings. |
| 1322 |
* |
| 1323 |
* @return void |
| 1324 |
* @since 3.2.0 |
| 1325 |
*/ |
| 1326 |
public function display_2fa_user_settings(): void { |
| 1327 |
if ( ( ! is_admin() || defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) ) { |
| 1328 |
wp_enqueue_script( 'wp-i18n' ); |
| 1329 |
|
| 1330 |
do_action( 'wd_2fa_form_before' ); |
| 1331 |
|
| 1332 |
echo '<form class="wpdef-2fa-wrap" action="" method="post">'; |
| 1333 |
|
| 1334 |
$this->show_user_profile( $this->current_user ); |
| 1335 |
|
| 1336 |
echo '<input type="hidden" name="action" value="save_def_2fa_user_settings" />'; |
| 1337 |
echo '<button type="submit" class="button" name="save_def_2fa_user_settings" value="' . esc_attr__( |
| 1338 |
'Save changes', |
| 1339 |
'defender-security' |
| 1340 |
) . '">' |
| 1341 |
. esc_html__( 'Save changes', 'defender-security' ) . '</button>'; |
| 1342 |
echo '</form>'; |
| 1343 |
|
| 1344 |
do_action( 'wd_2fa_form_after' ); |
| 1345 |
} else { |
| 1346 |
apply_filters( 'wd_2fa_form_when_not_logged_in', '' ); |
| 1347 |
} |
| 1348 |
} |
| 1349 |
|
| 1350 |
/** |
| 1351 |
* 1. Register new endpoint (URL) for My Account page. Re-save Permalinks or it will give 404 error. |
| 1352 |
* |
| 1353 |
* @return void |
| 1354 |
*/ |
| 1355 |
public function wp_defender_2fa_endpoint(): void { |
| 1356 |
add_rewrite_endpoint( $this->slug, EP_PERMALINK | EP_PAGES ); |
| 1357 |
} |
| 1358 |
|
| 1359 |
/** |
| 1360 |
* 2. dds the slug of the current instance to the given array of query variables. |
| 1361 |
* |
| 1362 |
* @param array $vars The array of query variables. |
| 1363 |
* |
| 1364 |
* @return array The updated array of query variables. |
| 1365 |
*/ |
| 1366 |
public function wp_defender_2fa_query_vars( $vars ) { |
| 1367 |
$vars[] = $this->slug; |
| 1368 |
|
| 1369 |
return $vars; |
| 1370 |
} |
| 1371 |
|
| 1372 |
/** |
| 1373 |
* 3. Inserts the new endpoint into the My Account menu. |
| 1374 |
* |
| 1375 |
* @param array $items The array of items in the My Account menu. |
| 1376 |
* |
| 1377 |
* @return array The updated array of items with the new endpoint inserted. |
| 1378 |
*/ |
| 1379 |
public function wp_defender_2fa_link_my_account( $items ) { |
| 1380 |
$needed_place = is_array( $items ) && array() !== $items ? ( count( $items ) - 1 ) : 0; |
| 1381 |
|
| 1382 |
return array_slice( $items, 0, $needed_place, true ) |
| 1383 |
+ array( $this->slug => esc_html__( '2FA', 'defender-security' ) ) |
| 1384 |
+ array_slice( $items, $needed_place, null, true ); |
| 1385 |
} |
| 1386 |
|
| 1387 |
/** |
| 1388 |
* 4. Add content to the new tab. |
| 1389 |
* |
| 1390 |
* @return void |
| 1391 |
*/ |
| 1392 |
public function wp_defender_2fa_content(): void { |
| 1393 |
echo do_shortcode( '[wp_defender_2fa_user_settings]' ); |
| 1394 |
} |
| 1395 |
|
| 1396 |
/** |
| 1397 |
* Save the 2fa details and redirect back to 'My Account' page. |
| 1398 |
* |
| 1399 |
* @return void |
| 1400 |
*/ |
| 1401 |
public function save_2fa_details() { |
| 1402 |
$action = defender_get_data_from_request( 'action', 'p' ); |
| 1403 |
if ( ! is_string( $action ) || '' === trim( $action ) || 'save_def_2fa_user_settings' !== $action ) { |
| 1404 |
return; |
| 1405 |
} |
| 1406 |
|
| 1407 |
wc_nocache_headers(); |
| 1408 |
|
| 1409 |
$user_id = $this->current_user->ID; |
| 1410 |
if ( $user_id <= 0 ) { |
| 1411 |
return; |
| 1412 |
} |
| 1413 |
// Verify nonce and other two-factor arguments passed. |
| 1414 |
$this->profile_update( $user_id ); |
| 1415 |
|
| 1416 |
wc_add_notice( esc_html__( 'Two-Factor settings updated successfully.', 'defender-security' ) ); |
| 1417 |
// @since 3.2.0 |
| 1418 |
do_action( 'wd_woocommerce_save_2fa_details', $user_id ); |
| 1419 |
|
| 1420 |
wp_safe_redirect( wc_get_endpoint_url( $this->slug, '', wc_get_page_permalink( 'myaccount' ) ) ); |
| 1421 |
exit; |
| 1422 |
} |
| 1423 |
|
| 1424 |
/** |
| 1425 |
* Enable provider slugs. |
| 1426 |
* |
| 1427 |
* @param array $provider_slugs The array of provider slugs to enable. |
| 1428 |
* |
| 1429 |
* @return void |
| 1430 |
*/ |
| 1431 |
public function enable_provider_slugs( array $provider_slugs ) { |
| 1432 |
// Track conditions. |
| 1433 |
if ( array() !== $provider_slugs ) { |
| 1434 |
$methods = array(); |
| 1435 |
foreach ( $this->service->get_providers() as $slug => $object ) { |
| 1436 |
if ( in_array( $slug, $provider_slugs, true ) ) { |
| 1437 |
$methods[] = $object->get_label(); |
| 1438 |
} |
| 1439 |
} |
| 1440 |
// Run track. |
| 1441 |
$this->track_feature( |
| 1442 |
'def_2fa_method_activated', |
| 1443 |
array( |
| 1444 |
'Method name' => $methods, |
| 1445 |
) |
| 1446 |
); |
| 1447 |
} |
| 1448 |
} |
| 1449 |
} |
| 1450 |
|