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 / webauthn.php

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

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