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

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

644 lines 19.4 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 Nyholm\Psr7\Factory\Psr17Factory;
6 use Nyholm\Psr7Server\ServerRequestCreator;
7 use Webauthn\AuthenticatorSelectionCriteria;
8 use Webauthn\PublicKeyCredentialRpEntity;
9 use Webauthn\PublicKeyCredentialCreationOptions;
10 use Webauthn\PublicKeyCredentialUserEntity;
11 use Webauthn\Server;
12 use WP_Defender\Component\Two_Fa as Two_Fa_Component;
13 use WP_Defender\Component\Webauthn as Webauthn_Component;
14 use WP_Defender\Component\Two_Factor\Providers\Webauthn as Webauthn_Provider;
15 use WP_Defender\Controller;
16 use WP_Defender\Traits\Webauthn as Webauthn_Trait;
17 use Exception;
18 use Error;
19
20 /**
21 * Class Webauthn
22 * @package WP_Defender\Controller
23 * @since 3.0.0
24 */
25 class Webauthn extends Controller {
26 use Webauthn_Trait;
27
28 /**
29 * @var Webauthn_Component
30 */
31 protected $service;
32
33 public const ALLOWED_AUTH_TYPES = [ 'platform', 'cross-platform' ];
34
35 /**
36 * Constructor.
37 */
38 public function __construct() {
39 $this->service = wd_di()->get( Webauthn_Component::class );
40
41 if ( $this->service->check_webauthn_requirements() ) {
42 // Register a new authenticator.
43 add_action( 'wp_ajax_defender_webauthn_create_challenge', [ $this, 'create_challenge' ] );
44 add_action( 'wp_ajax_defender_webauthn_verify_challenge', [ $this, 'verify_challenge' ] );
45 // Remove authenticator.
46 add_action( 'wp_ajax_defender_webauthn_remove_authenticator', [ $this, 'remove_authenticator' ] );
47 // Rename authenticator.
48 add_action( 'wp_ajax_defender_webauthn_rename_authenticator', [ $this, 'rename_authenticator' ] );
49 // Verify user's device.
50 add_action( 'wp_ajax_defender_webauthn_get_option', [ $this, 'get_credential_request_option' ] );
51 add_action( 'wp_ajax_nopriv_defender_webauthn_get_option', [ $this, 'get_credential_request_option' ] );
52 add_action( 'wp_ajax_defender_webauthn_verify_response', [ $this, 'verify_response' ] );
53 add_action( 'wp_ajax_nopriv_defender_webauthn_verify_response', [ $this, 'verify_response' ] );
54 }
55 }
56
57 /**
58 * Get authenticator records for current user.
59 *
60 * @param int $user_id
61 *
62 * @return array
63 */
64 public function get_current_user_authenticators( int $user_id ): array {
65 $arr = [];
66
67 $user_entity = $this->get_user_entity( $user_id );
68 if ( false === $user_entity ) {
69 return $arr;
70 }
71
72 $user_entity_id = $user_entity->getId();
73 $user_credentials = $this->service->getCredentials();
74 if ( ! empty( $user_credentials ) && is_array( $user_credentials ) ) {
75 foreach ( $user_credentials as $key => $value ) {
76 if ( $user_entity_id === $value['user'] ) {
77 $arr[] = [
78 'key' => $this->base64url_encode( $key ),
79 'label' => $value['label'],
80 'added' => gmdate( 'Y-m-d', $value['added'] ),
81 'auth_type' => $value['authenticator_type'],
82 ];
83 }
84 }
85 }
86
87 return $arr;
88 }
89
90 /**
91 * Get user entity.
92 *
93 * @param int $user_id
94 *
95 * @return false|PublicKeyCredentialUserEntity
96 */
97 public function get_user_entity( int $user_id ) {
98 if ( $user_id <= 0 ) {
99 return false;
100 }
101
102 $user = get_user_by( 'id', $user_id );
103 if ( ! is_object( $user ) ) {
104 return false;
105 }
106
107 $user_hash = $this->get_user_hash( $user->user_login, $user->display_name );
108 $user_avatar = get_avatar_url( $user->user_email, [ 'scheme' => 'https' ] );
109
110 return new PublicKeyCredentialUserEntity(
111 $user->user_login,
112 $user_hash,
113 $user->display_name,
114 $user_avatar
115 );
116 }
117
118 /**
119 * Create challenge.
120 *
121 * @return void
122 */
123 public function create_challenge(): void {
124 try {
125 if ( ! $this->verify_nonce( 'wpdef_webauthn' ) ) {
126 throw new Exception( __( 'Bad nonce.', 'wpdef' ) );
127 }
128
129 if ( empty( $_GET['type'] ) ) {
130 throw new Exception( __( 'Missing field(s).', 'wpdef' ) );
131 }
132
133 $user_id = get_current_user_id();
134 $user_entity = $this->get_user_entity( $user_id );
135 if ( false === $user_entity ) {
136 throw new Exception( __( 'User does not exist.', 'wpdef' ) );
137 }
138
139 // Get the list of all authenticators associated to a user.
140 $credential_sources = $this->service->findAllForUserEntity( $user_entity );
141
142 // Convert the Credential Sources into Public Key Credential Descriptors
143 $exclude_credentials = array_map(
144 function ( $credential ) {
145 return $credential->getPublicKeyCredentialDescriptor();
146 },
147 $credential_sources
148 );
149
150 $auth_type = sanitize_text_field( $_GET['type'] );
151 if ( 'platform' === $auth_type ) {
152 $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM;
153 } elseif ( 'cross-platform' === $auth_type ) {
154 $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
155 } else {
156 $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE;
157 }
158
159 // Create authenticator selection.
160 $resident_key = false;
161 $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
162 $authenticator_selection_criteria = new AuthenticatorSelectionCriteria(
163 $authenticator_type,
164 $resident_key,
165 $user_verification
166 );
167
168 $rp_entity = new PublicKeyCredentialRpEntity(
169 $this->get_site_name(),
170 $this->get_site_domain()
171 );
172 $server = new Server(
173 $rp_entity,
174 $this->service,
175 null
176 );
177
178 $public_key_credential_creation_options = $server->generatePublicKeyCredentialCreationOptions(
179 $user_entity,
180 PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
181 $exclude_credentials,
182 $authenticator_selection_criteria
183 );
184
185 $client_id = time() . defender_generate_random_string( 24 );
186
187 // set transition for later use.
188 $this->set_trans_val( 'pub_key_cco', base64_encode( serialize( $public_key_credential_creation_options ) ), $client_id );
189
190 // Send Challenge.
191 $public_key_credential_creation_options = json_decode( wp_json_encode( $public_key_credential_creation_options ), true );
192 $public_key_credential_creation_options['clientID'] = $client_id;
193 wp_send_json_success( $public_key_credential_creation_options );
194 } catch ( Error $error ) {
195 wp_send_json_error( $error->getMessage() );
196 } catch ( Exception $exception ) {
197 wp_send_json_error( $exception->getMessage() );
198 }
199 }
200
201 /**
202 * Verify challenge.
203 *
204 * @return void
205 */
206 public function verify_challenge(): void {
207 $client_id = null;
208 try {
209 if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) {
210 throw new Exception( __( 'Bad nonce.', 'wpdef' ) );
211 }
212
213 if ( empty( $_POST['data'] ) || empty( $_POST['client_id'] ) ) {
214 throw new Exception( __( 'Missing field(s).', 'wpdef' ) );
215 }
216
217 $psr17_factory = new Psr17Factory();
218 $creator = new ServerRequestCreator(
219 $psr17_factory,
220 $psr17_factory,
221 $psr17_factory,
222 $psr17_factory
223 );
224
225 $server_request = $creator->fromGlobals();
226
227 $rp_entity = new PublicKeyCredentialRpEntity(
228 $this->get_site_name(),
229 $this->get_site_domain()
230 );
231
232 $server = new Server(
233 $rp_entity,
234 $this->service,
235 null
236 );
237
238 $response_data = base64_decode( sanitize_text_field( $_POST['data'] ) );
239 $client_id = sanitize_text_field( $_POST['client_id'] );
240 $pub_key_cco = $this->get_trans_val( 'pub_key_cco', $client_id );
241 $pub_key_cco = unserialize( base64_decode( $pub_key_cco ) );
242
243 $public_key_credential_source = $server->loadAndCheckAttestationResponse(
244 $response_data,
245 $pub_key_cco,
246 $server_request
247 );
248
249 $this->service->saveCredentialSource( $public_key_credential_source );
250
251 // Delete transient
252 $this->delete_trans( 'pub_key_cco', $client_id );
253
254 $response = [];
255 $cred_data = $this->service->getCredentials();
256 $cred_id = base64_encode( $public_key_credential_source->getPublicKeyCredentialId() );
257 if ( isset( $cred_data[ $cred_id ] ) ) {
258 $response = [
259 'key' => $this->base64url_encode( $cred_id ),
260 'label' => ucfirst( $cred_data[ $cred_id ]['label'] ),
261 'added' => gmdate( 'Y-m-d', $cred_data[ $cred_id ]['added'] ),
262 'auth_type' => $cred_data[ $cred_id ]['authenticator_type'],
263 ];
264 }
265 wp_send_json_success( $response );
266 } catch ( Error $error ) {
267 $this->delete_trans( 'pub_key_cco', $client_id );
268 wp_send_json_error( $error->getMessage() );
269 } catch ( Exception $exception ) {
270 $this->delete_trans( 'pub_key_cco', $client_id );
271 wp_send_json_error( $exception->getMessage() );
272 }
273 }
274
275 /**
276 * Remove authenticator.
277 *
278 * @return void
279 */
280 public function remove_authenticator(): void {
281 try {
282 if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) {
283 throw new Exception( __( 'Bad nonce.', 'wpdef' ) );
284 }
285
286 if ( empty( $_POST['key'] ) ) {
287 throw new Exception( __( 'Missing field(s).', 'wpdef' ) );
288 }
289
290 $cred_id = sanitize_text_field( $_POST['key'] );
291 $cred_id = $this->base64url_decode( $cred_id );
292
293 $user_id = get_current_user_id();
294 $user_entity = $this->get_user_entity( $user_id );
295 if ( false === $user_entity ) {
296 throw new Exception( __( 'User does not exist.', 'wpdef' ) );
297 }
298
299 $is_removed = false;
300 $credential_sources = $this->service->findAllForUserEntity( $user_entity );
301 if ( ! empty( $credential_sources ) && is_array( $credential_sources ) ) {
302 foreach ( $credential_sources as $credential_source ) {
303 if ( base64_encode( $credential_source->getPublicKeyCredentialId() ) === $cred_id ) {
304 $option_user_credentials = $this->service->getCredentials();
305 if ( isset( $option_user_credentials[ $cred_id ] ) ) {
306 unset( $option_user_credentials[ $cred_id ] );
307 $this->service->setCredentials( $option_user_credentials );
308
309 if ( 0 === ( count( $credential_sources ) - 1 ) ) {
310 $enabled_providers = get_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, true );
311 if ( empty( $enabled_providers ) ) {
312 $enabled_providers = [];
313 }
314 $key = array_search( Webauthn_Provider::$slug, $enabled_providers, true );
315 if ( false !== $key ) {
316 unset( $enabled_providers[ $key ] );
317 update_user_meta( $user_id, Two_Fa_Component::ENABLED_PROVIDERS_USER_KEY, $enabled_providers );
318 }
319 }
320
321 $is_removed = true;
322 }
323 break;
324 }
325 }
326 }
327
328 if ( true === $is_removed ) {
329 wp_send_json_success( __( 'Authenticator removed.', 'wpdef' ) );
330 } else {
331 wp_send_json_error( __( 'Key did not match any registered authenticator.', 'wpdef' ) );
332 }
333 } catch ( Error $error ) {
334 wp_send_json_error( $error->getMessage() );
335 } catch ( Exception $exception ) {
336 wp_send_json_error( $exception->getMessage() );
337 }
338 }
339
340 /**
341 * Rename authenticator.
342 *
343 * @return void
344 */
345 public function rename_authenticator(): void {
346 try {
347 if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) {
348 throw new Exception( __( 'Bad nonce.', 'wpdef' ) );
349 }
350
351 if ( empty( $_POST['key'] ) || empty( $_POST['label'] ) ) {
352 throw new Exception( __( 'Missing field(s).', 'wpdef' ) );
353 }
354
355 $cred_id = sanitize_text_field( $_POST['key'] );
356 $cred_id = $this->base64url_decode( $cred_id );
357 $new_label = sanitize_text_field( $_POST['label'] );
358
359 $user_id = get_current_user_id();
360 $user_entity = $this->get_user_entity( $user_id );
361 if ( false === $user_entity ) {
362 throw new Exception( __( 'User does not exist.', 'wpdef' ) );
363 }
364
365 $is_renamed = false;
366 $credential_sources = $this->service->findAllForUserEntity( $user_entity );
367 if ( ! empty( $credential_sources ) && is_array( $credential_sources ) ) {
368 foreach ( $credential_sources as $credential_source ) {
369 if ( base64_encode( $credential_source->getPublicKeyCredentialId() ) === $cred_id ) {
370 $option_user_credentials = $this->service->getCredentials();
371
372 if ( isset( $option_user_credentials[ $cred_id ] ) ) {
373 $option_user_credentials[ $cred_id ]['label'] = $new_label;
374 $this->service->setCredentials( $option_user_credentials );
375 $is_renamed = true;
376 break;
377 }
378 }
379 }
380 }
381
382 if ( true === $is_renamed ) {
383 wp_send_json_success( __( 'Authenticator identifier renamed.', 'wpdef' ) );
384 } else {
385 wp_send_json_error( __( 'Key did not match any registered authenticator.', 'wpdef' ) );
386 }
387 } catch ( Error $error ) {
388 wp_send_json_error( $error->getMessage() );
389 } catch ( Exception $exception ) {
390 wp_send_json_error( $exception->getMessage() );
391 }
392 }
393
394 /**
395 * Get credential request option.
396 *
397 * @return void
398 */
399 public function get_credential_request_option(): void {
400 try {
401 if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) {
402 throw new Exception( __( 'Bad nonce.', 'wpdef' ) );
403 }
404
405 if ( empty( $_POST['username'] ) ) {
406 throw new Exception( __( 'Missing field(s).', 'wpdef' ) );
407 }
408
409 $rp_entity = new PublicKeyCredentialRpEntity(
410 $this->get_site_name(),
411 $this->get_site_domain()
412 );
413
414 $server = new Server(
415 $rp_entity,
416 $this->service,
417 null
418 );
419
420 $user_entity = false;
421 $username = sanitize_text_field( $_POST['username'] );
422 $user = get_user_by( 'login', $username );
423 if ( is_object( $user ) ) {
424 $user_entity = $this->get_user_entity( $user->ID );
425 }
426
427 if ( false === $user_entity ) {
428 throw new Exception( __( 'User does not exist.', 'wpdef' ) );
429 }
430
431 $auth_type = ! empty( $_POST['type'] ) ? sanitize_text_field( $_POST['type'] ): null;
432 if ( in_array( $auth_type, self::ALLOWED_AUTH_TYPES ) ) {
433 $credential_sources = $this->service->findAllForUserEntityByType( $user_entity, $auth_type );
434 } else {
435 $credential_sources = $this->service->findAllForUserEntity( $user_entity );
436 }
437
438 if ( ! is_array( $credential_sources ) || 0 === count( $credential_sources ) ) {
439 throw new Exception( __( 'Please register a device first to authenticate it.', 'wpdef' ), 100 );
440 }
441
442 // Convert the Credential Sources into Public Key Credential Descriptors for excluding
443 $allowed_credentials = array_map(
444 function( $credential ) {
445 return $credential->getPublicKeyCredentialDescriptor();
446 },
447 $credential_sources
448 );
449 $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
450
451 $public_key_credential_request_options = $server->generatePublicKeyCredentialRequestOptions(
452 $user_verification,
453 $allowed_credentials
454 );
455
456 // set transition for later use.
457 $client_id = time() . defender_generate_random_string( 24 );
458 $this->set_trans_val( 'pub_key_cro', base64_encode( serialize( $public_key_credential_request_options ) ), $client_id );
459
460 $public_key_credential_request_options = json_decode( wp_json_encode( $public_key_credential_request_options ), true );
461 $public_key_credential_request_options['clientID'] = $client_id;
462 wp_send_json_success( $public_key_credential_request_options );
463 } catch ( Error $error ) {
464 wp_send_json_error( $error->getMessage() );
465 } catch ( Exception $exception ) {
466 wp_send_json_error(
467 [
468 'message' => $exception->getMessage(),
469 'code' => $exception->getCode(),
470 ]
471 );
472 }
473 }
474
475 /**
476 * Verify response.
477 *
478 * @param bool $return Either return array or echo json.
479 *
480 * @return array|void
481 */
482 public function verify_response( bool $return = false ) {
483 $client_id = null;
484 try {
485 if ( ! $this->verify_nonce( 'wpdef_webauthn', 'post' ) ) {
486 throw new Exception( __( 'Bad nonce.', 'wpdef' ) );
487 }
488
489 if ( empty( $_POST['data'] ) || empty( $_POST['username'] ) || empty( $_POST['client_id'] ) ) {
490 throw new Exception( __( 'Missing field(s).', 'wpdef' ) );
491 }
492
493 $user_entity = false;
494 $response_data = base64_decode( $_POST['data'] );
495 $username = sanitize_text_field( $_POST['username'] );
496 $client_id = sanitize_text_field( $_POST['client_id'] );
497 $pub_key_cro = unserialize( base64_decode( $this->get_trans_val( 'pub_key_cro', $client_id ) ) );
498 $user = get_user_by( 'login', $username );
499
500 if ( is_object( $user ) ) {
501 $user_entity = $this->get_user_entity( $user->ID );
502 }
503
504 if ( false === $user_entity ) {
505 throw new Exception( __( 'User does not exist.', 'wpdef' ) );
506 }
507
508 $psr17_factory = new Psr17Factory();
509 $creator = new ServerRequestCreator(
510 $psr17_factory,
511 $psr17_factory,
512 $psr17_factory,
513 $psr17_factory
514 );
515
516 $server_request = $creator->fromGlobals();
517
518 $rp_entity = new PublicKeyCredentialRpEntity(
519 $this->get_site_name(),
520 $this->get_site_domain()
521 );
522
523 $server = new Server(
524 $rp_entity,
525 $this->service,
526 null
527 );
528
529 $server->loadAndCheckAssertionResponse(
530 $response_data,
531 $pub_key_cro,
532 $user_entity,
533 $server_request
534 );
535
536 // Success
537 $this->delete_trans( 'pub_key_cro', $client_id );
538
539 $data = __( 'Authenticator verified successfully.', 'wpdef' );
540 return defender_maybe_echo_json( $data, true, $return );
541 } catch ( Error $error ) {
542 $this->delete_trans( 'pub_key_cro', $client_id );
543 return defender_maybe_echo_json( $error->getMessage(), false, $return );
544 } catch ( Exception $exception ) {
545 $this->delete_trans( 'pub_key_cro', $client_id );
546 return defender_maybe_echo_json( $exception->getMessage(), false, $return );
547 }
548 }
549
550 /**
551 * Get translation.
552 *
553 * @return array
554 */
555 public function get_translations(): array {
556 $translations = [
557 'registration_start' => __( 'Registering a new authenticator is in process.', 'wpdef' ),
558 'authenticator_reg_success' => __( 'Registered new authenticator.', 'wpdef' ),
559 'authenticator_reg_failed' => __( 'ERROR: Something went wrong.', 'wpdef' ),
560 'multiple_reg_attempt' => __( 'Registration failed! The authenticator you are trying to register is already registered with your account.', 'wpdef' ),
561 'authentication_start' => __( 'Authenticating', 'wpdef' ),
562 'authenticator_verification_success' => __( 'Authenticated device successfully.', 'wpdef' ),
563 'authenticator_verification_failed' => __( 'Authentication verification failed! Please make sure that biometric functionality is configured on your phone.', 'wpdef' ),
564 'remove_auth' => __( 'Are you sure you want to remove authenticator?', 'wpdef' ),
565 'login_failed' => __( 'ERROR: Verification failed.', 'wpdef' ),
566 'client_webauthn_notice' => __( 'WebAuth is not supported by your web browser. Please install an updated version, or try another browser.', 'wpdef' ),
567 ];
568
569 if ( ( new \WP_Defender\Behavior\WPMUDEV() )->show_support_links() ) {
570 $translations['authenticator_verification_failed'] .= sprintf(
571 /* translators: ... */
572 __( ' Still having trouble?&nbsp;<a target="_blank" href="%s">Open a support ticket</a>.', 'wpdef' ),
573 'https://wpmudev.com/forums/forum/support#question'
574 );
575 }
576
577 return $translations;
578 }
579
580 /**
581 * Remove data.
582 *
583 * @return void
584 */
585 public function remove_data(): void {
586 global $wpdb;
587 if ( is_multisite() ) {
588 $offset = 0;
589 $limit = 100;
590 while ( $blogs = $wpdb->get_results( "SELECT blog_id FROM {$wpdb->blogs} LIMIT {$offset}, {$limit}", ARRAY_A ) ) {
591 if ( ! empty( $blogs ) && is_array( $blogs ) ) {
592 foreach ( $blogs as $blog ) {
593 switch_to_blog( $blog['blog_id'] );
594
595 $sql = $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s;", "%{$this->option_prefix}%" );
596 $wpdb->query( $sql );
597
598 restore_current_blog();
599 }
600 }
601 $offset += $limit;
602 }
603 } else {
604 $sql = $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s;", "%{$this->option_prefix}%" );
605 $wpdb->query( $sql );
606 }
607 }
608
609 /**
610 * Export strings.
611 *
612 * @return array
613 */
614 public function export_strings(): array {
615 return [];
616 }
617
618 /**
619 * @return array
620 */
621 public function to_array(): array {
622 return [];
623 }
624
625 /**
626 * @param array $data
627 *
628 * @return void
629 */
630 public function import_data( $data ): void {}
631
632 /**
633 * @return void
634 */
635 public function remove_settings(): void {}
636
637 /**
638 * @return array
639 */
640 public function data_frontend(): array {
641 return [];
642 }
643 }
644