PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.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 / component / two-factor / providers / class-webauthn.php

class-webauthn.php in Defender Security – Malware Scanner, Login Security & Firewall 5.5.1, at src/component/two-factor/providers/class-webauthn.php

439 lines 13.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles Web Authentication (WebAuthn) for Two-Factor Authentication.
4 *
5 * @package WP_Defender\Component\Two_Factor\Providers
6 */
7
8 namespace WP_Defender\Component\Two_Factor\Providers;
9
10 use WP_User;
11 use WP_Error;
12 use WP_Defender\Component\Two_Fa;
13 use WP_Defender\Behavior\WPMUDEV;
14 use WP_Defender\Traits\Webauthn as Webauthn_Trait;
15 use WP_Defender\Component\Webauthn as Webauthn_Component;
16 use WP_Defender\Component\Two_Factor\Two_Factor_Provider;
17 use WP_Defender\Controller\Webauthn as Webauthn_Controller;
18
19 /**
20 * Handle Web Authentication (WebAuthn) for Two-Factor Authentication.
21 *
22 * @since 3.0.0
23 */
24 class Webauthn extends Two_Factor_Provider {
25
26 use Webauthn_Trait;
27
28 /**
29 * 2fa provider slug.
30 *
31 * @var string
32 */
33 public static $slug = 'webauthn';
34
35 /**
36 * Constructor for the Biometric provider.
37 */
38 public function __construct() {
39 add_action( 'wd_2fa_user_options_' . self::$slug, array( $this, 'user_options' ) );
40 }
41
42 /**
43 * Get the name of the provider.
44 *
45 * @return string
46 */
47 public function get_label(): string {
48 return $this->get_user_label();
49 }
50
51 /**
52 * Get login label.
53 *
54 * @return string
55 */
56 public function get_login_label(): string {
57 return $this->get_user_label();
58 }
59
60 /**
61 * Get user label.
62 *
63 * @return string
64 */
65 public function get_user_label(): string {
66 return esc_html__( 'Web Authentication', 'defender-security' );
67 }
68
69 /**
70 * Get the desc of the provider.
71 *
72 * @return string
73 */
74 public function get_description(): string {
75 return esc_html__(
76 "Authenticate login using your device's built-in fingerprint, facial recognition, or an external hardware security key.",
77 'defender-security'
78 );
79 }
80
81 /**
82 * Display auth method.
83 *
84 * @param WP_User $user WP_User object of the logged-in user.
85 */
86 public function user_options( WP_User $user ): void {
87 $notices = array(
88 array(
89 'type' => 'info',
90 'extra_class' => 'additional-2fa-method',
91 'style' => 'display: none',
92 'message' => esc_html__(
93 'Access from unregistered devices will be denied. To avoid this, consider setting up an additional authentication method.',
94 'defender-security'
95 ),
96 ),
97 );
98
99 $missed_requirement_message = $this->missed_requirement_message();
100 $user_has_admin_capability = current_user_can( 'manage_options' );
101
102 if ( $user_has_admin_capability ) {
103 $notice_message = $missed_requirement_message['warning'];
104 } else {
105 $notice_message = esc_html__(
106 'Your site doesn\'t meet the web authentication requirement. Please contact your site\'s administrator to fix this issue.',
107 'defender-security'
108 );
109 }
110
111 $all_server_requirement_passed = $missed_requirement_message['all_server_requirement_passed'];
112
113 if ( $all_server_requirement_passed ) {
114 $message = $missed_requirement_message['browser_message'];
115 $extra_class = ' browser-notice ';
116 } else {
117 $message = $notice_message;
118 $extra_class = ' has-server-error ';
119 }
120
121 $notices[] = array(
122 'type' => 'warning',
123 'extra_class' => $extra_class,
124 'style' => '',
125 'message' => $message,
126 );
127
128 $translations = wd_di()->get( Webauthn_Controller::class )->get_translations();
129 $notices[] = array(
130 'type' => 'warning',
131 'extra_class' => ' user-handle-mismatch ',
132 'style' => 'display: none',
133 'is_dismissible' => true,
134 'message' => $translations['user_handle_mismatch_main_notice'],
135
136 );
137
138 $this->get_controller()->render_partial(
139 'two-fa/providers/biometric',
140 array(
141 'notices' => $notices,
142 )
143 );
144 }
145
146 /**
147 * Whether this 2FA provider is configured and available for the user specified.
148 *
149 * @param WP_User $user WP_User object of the logged-in user.
150 *
151 * @return bool
152 */
153 public function is_available_for_user( WP_User $user ): bool {
154 return true;
155 }
156
157 /**
158 * Authentication form markup.
159 *
160 * @return void
161 */
162 public function authentication_form() {
163 ?>
164 <div class="welcome-screen">
165 <p class="wpdef-2fa-label"><?php echo $this->get_login_label(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
166 <p><?php esc_html_e( 'Select an authentication method to continue.', 'defender-security' ); ?></p>
167 <div class="option-row" data-authType="platform">
168 <span class="icon biometric"></span>
169 <span>
170 <strong class="option-hd"><?php esc_html_e( 'Biometric Authentication', 'defender-security' ); ?></strong>
171 <?php esc_html_e( 'Use fingerprint or facial recognition', 'defender-security' ); ?>
172 </span>
173 </div>
174 <div class="option-row" data-authType="cross-platform">
175 <span class="icon hardware"></span>
176 <span>
177 <strong class="option-hd"><?php esc_html_e( 'Hardware Key Authentication', 'defender-security' ); ?></strong>
178 <?php esc_html_e( 'Use USB security keys to login', 'defender-security' ); ?>
179 </span>
180 </div>
181 </div>
182 <div class="webauthn-content-wrap">
183 <div class="webauthn-platform" style="display:none;">
184 <p class="wpdef-2fa-label"><?php esc_html_e( 'Biometric Authentication', 'defender-security' ); ?></p>
185 <p class="wpdef-2fa-text">
186 <?php
187 esc_html_e(
188 'Please verify your identity using fingerprint or facial recognition.',
189 'defender-security'
190 );
191 ?>
192 </p>
193 </div>
194 <div class="webauthn-cross-platform" style="display:none;">
195 <p class="wpdef-2fa-label"><?php esc_html_e( 'Hardware Key Authentication', 'defender-security' ); ?></p>
196 <p class="wpdef-2fa-text">
197 <?php esc_html_e( 'Insert and tap on your USB security key to login.', 'defender-security' ); ?>
198 </p>
199 </div>
200 <p class="wpdef-2fa-text wpdef-2fa-biometric-process-desc" style="display: none;">
201 <img class="def-loader" src="<?php defender_asset_url( '/assets/img/spinner.svg', true ); ?>"
202 alt="loading">
203 <?php esc_html_e( 'Verification in process...', 'defender-security' ); ?>
204 </p>
205 <div class="wpdef-2fa-webauthn-control" style="display:none;">
206 <button type="button" class="button"
207 id="wpdef-2fa-webauthn-back-btn"><?php esc_html_e( 'Back', 'defender-security' ); ?></button>
208 <button type="button" class="button button-primary float-r" id="wpdef-2fa-biometric-retry-btn">
209 <span class="dashicons dashicons-update"></span>
210 <?php esc_html_e( 'Retry', 'defender-security' ); ?>
211 </button>
212 </div>
213 <input type="hidden" id="wpdef-2fa-biometric-data" name="data"/>
214 <input type="hidden" id="wpdef-2fa-biometric-username" name="username"/>
215 <input type="hidden" id="wpdef-2fa-biometric-client-id" name="client_id"/>
216 <input type="hidden" id="wpdef-2fa-biometric-nonce" name="_def_nonce"/>
217 <button id="wpdef-2fa-biometric-submit-btn" type="submit" style="display:none;"></button>
218 </div>
219 <?php
220 }
221
222 /**
223 * Whether this 2FA provider is configured and available for the user specified.
224 *
225 * @param WP_User $user The user to authenticate.
226 *
227 * @return bool|WP_Error
228 */
229 public function validate_authentication( WP_User $user ) {
230 $webauthn_controller = wd_di()->get( Webauthn_Controller::class );
231 $response = $webauthn_controller->verify_response( true );
232
233 if ( isset( $response['success'] ) && true === $response['success'] ) {
234 return true;
235 } else {
236 // No attempt-checks.
237 $translations = $webauthn_controller->get_translations();
238
239 if ( 'Invalid user handle' === $response['data']['message'] ) {
240 $message = $translations['login_user_handle_match_failed'];
241 } else {
242 $message = $translations['login_failed'];
243 }
244
245 return new WP_Error( 'opt_fail', $message );
246 }
247 }
248
249 /**
250 * Create missed requirement message.
251 * Create dynamic message with boolean flag for all server requirement passed.
252 *
253 * @return array Dynamic message with boolean flag for all server requirement passed.
254 */
255 private function missed_requirement_message(): array {
256 $warning_count = 0;
257 $all_server_requirement_passed = true;
258 $warning_core_message = '';
259
260 if ( ! $this->is_ssl() ) {
261 ++$warning_count;
262 $all_server_requirement_passed = false;
263 $warning_core_message .= esc_html__( 'SSL certificate or HTTPS is not forced. ', 'defender-security' );
264 }
265
266 $extension_warning_message = $this->get_extension_warning_message();
267
268 if ( '' !== $extension_warning_message ) {
269 $warning_count += count( $this->get_failed_extension() );
270 $all_server_requirement_passed = false;
271 $warning_core_message .= $extension_warning_message;
272 }
273
274 $browser_extension_message = '<span class="browser-incompatible-msg">';
275 $browser_extension_message .= sprintf(
276 /* translators: %s: Browser version */
277 esc_html__(
278 'The current browser version %s is not compatible with this feature, please update your browser version, or try a different one. ',
279 'defender-security'
280 ),
281 '<span class="browser-version">' . $this->get_browser_name() . '</span>'
282 );
283 $browser_extension_message .= '</span>';
284
285 $warning_prefix = _n(
286 'Your site doesn\'t meet the web authentication requirement. ',
287 'Your site doesn\'t meet the web authentication requirements. ',
288 $warning_count,
289 'defender-security'
290 );
291 $warning_suffix = $this->get_warning_suffix();
292
293 $warning = $warning_prefix . $warning_core_message . $browser_extension_message . $warning_suffix;
294
295 $browser_message = $warning_prefix . $browser_extension_message . $warning_suffix;
296
297 return array(
298 'warning' => $warning,
299 'all_server_requirement_passed' => $all_server_requirement_passed,
300 'browser_message' => $browser_message,
301 );
302 }
303
304 /**
305 * Get list of failed extensions.
306 *
307 * @return array Array of failed extensions or empty array if all extensions exists.
308 */
309 private function get_failed_extension(): array {
310 $failed_extensions = array();
311
312 if ( ! $this->is_enabled_gmp() ) {
313 $failed_extensions[] = 'GMP';
314 }
315
316 if ( ! $this->is_enabled_mbstring() ) {
317 $failed_extensions[] = 'MBSTRING';
318 }
319
320 if ( ! $this->is_enabled_sodium() ) {
321 $failed_extensions[] = 'SODIUM';
322 }
323
324 return $failed_extensions;
325 }
326
327 /**
328 * Concatenate multiple failed extensions.
329 * Form a comma separated with and before last element string from array of elements, if elements is more than two.
330 *
331 * @param array $extensions list of failed extensions.
332 *
333 * @return string Return concatenated string on success else empty string.
334 */
335 private function concatenate_failed_extensions( array $extensions ): string {
336 if ( count( $extensions ) === 1 ) {
337 return $extensions[0];
338 }
339
340 if ( count( $extensions ) > 1 ) {
341 $last_extension = esc_html__( ' and ', 'defender-security' ) . array_pop( $extensions );
342
343 return implode( ', ', $extensions ) . $last_extension;
344 }
345
346 return '';
347 }
348
349 /**
350 * Create extension warning message
351 *
352 * @return string Warning message if extension not met the requirement or empty string for met requirement.
353 */
354 private function get_extension_warning_message(): string {
355 $message = '';
356 $failed_extensions = $this->get_failed_extension();
357 $failed_extensions_count = count( $failed_extensions );
358
359 if ( $failed_extensions_count > 0 ) {
360 $extension_string = $this->concatenate_failed_extensions( $failed_extensions );
361
362 $message .= sprintf(
363 /* translators: %s: Missing PHP extension(s) */
364 _n(
365 'The <strong>%s extension</strong> is not active on your server. ',
366 'The <strong>%s extensions</strong> are not active on your server. ',
367 $failed_extensions_count,
368 'defender-security'
369 ),
370 $extension_string
371 );
372 }
373
374 return $message;
375 }
376
377 /**
378 * Retrieves the browser name and optionally the version.
379 *
380 * @param bool $with_version Whether to include the version number.
381 *
382 * @return string Browser name, optionally including the version.
383 */
384 private function get_browser_name( $with_version = true ): string {
385 if ( ! function_exists( 'wp_check_browser_version' ) ) {
386 include_once ABSPATH . 'wp-admin/includes/dashboard.php';
387 }
388
389 $browser = wp_check_browser_version();
390 $browser_string = $browser['name'] . ( $with_version ? ' ' . $browser['version'] : '' );
391
392 return $browser_string;
393 }
394
395 /**
396 * Determine the warning suffix.
397 *
398 * @return string Warning suffix sentence.
399 */
400 private function get_warning_suffix(): string {
401 $warning_suffix = '';
402 if ( ( new WPMUDEV() )->show_support_links() ) {
403 $warning_suffix = defender_support_ticket_text();
404 }
405
406 return $warning_suffix;
407 }
408
409 /**
410 * Check if OTP screen should be shown to user.
411 *
412 * @param WP_User $user The user to check.
413 *
414 * @return bool
415 * @since 3.3.0
416 */
417 public function is_otp_screen_available( WP_User $user ): bool {
418 $is_available = false;
419 $service_two_fa = wd_di()->get( Two_Fa::class );
420 $enabled_providers = $service_two_fa->get_enabled_providers_for_user( $user );
421
422 if ( ! empty( $user->ID ) && true === in_array( self::$slug, $enabled_providers, true ) ) {
423 $controller = wd_di()->get( Webauthn_Controller::class );
424 $service_webauthn = wd_di()->get( Webauthn_Component::class );
425 $user_entity = $controller->get_user_entity( $user->ID );
426
427 if ( false !== $user_entity ) {
428 $credential_sources = $service_webauthn->findAllForUserEntity( $user_entity );
429
430 if ( is_array( $credential_sources ) && 0 < count( $credential_sources ) ) {
431 $is_available = true;
432 }
433 }
434 }
435
436 return $is_available;
437 }
438 }
439