PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
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 6.2.2, at src/component/two-factor/providers/class-webauthn.php

440 lines 13.2 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 // Pass $user explicitly to avoid broken binding between authentication stages.
232 $response = $webauthn_controller->verify_response( true, $user );
233
234 if ( isset( $response['success'] ) && true === $response['success'] ) {
235 return true;
236 } else {
237 // No attempt-checks.
238 $translations = $webauthn_controller->get_translations();
239
240 if ( 'Invalid user handle' === $response['data']['message'] ) {
241 $message = $translations['login_user_handle_match_failed'];
242 } else {
243 $message = $translations['login_failed'];
244 }
245
246 return new WP_Error( 'opt_fail', $message );
247 }
248 }
249
250 /**
251 * Create missed requirement message.
252 * Create dynamic message with boolean flag for all server requirement passed.
253 *
254 * @return array Dynamic message with boolean flag for all server requirement passed.
255 */
256 private function missed_requirement_message(): array {
257 $warning_count = 0;
258 $all_server_requirement_passed = true;
259 $warning_core_message = '';
260
261 if ( ! $this->is_ssl() ) {
262 ++$warning_count;
263 $all_server_requirement_passed = false;
264 $warning_core_message .= esc_html__( 'SSL certificate or HTTPS is not forced. ', 'defender-security' );
265 }
266
267 $extension_warning_message = $this->get_extension_warning_message();
268
269 if ( '' !== $extension_warning_message ) {
270 $warning_count += count( $this->get_failed_extension() );
271 $all_server_requirement_passed = false;
272 $warning_core_message .= $extension_warning_message;
273 }
274
275 $browser_extension_message = '<span class="browser-incompatible-msg">';
276 $browser_extension_message .= sprintf(
277 /* translators: %s: Browser version */
278 esc_html__(
279 'The current browser version %s is not compatible with this feature, please update your browser version, or try a different one. ',
280 'defender-security'
281 ),
282 '<span class="browser-version">' . $this->get_browser_name() . '</span>'
283 );
284 $browser_extension_message .= '</span>';
285
286 $warning_prefix = _n(
287 'Your site doesn\'t meet the web authentication requirement. ',
288 'Your site doesn\'t meet the web authentication requirements. ',
289 $warning_count,
290 'defender-security'
291 );
292 $warning_suffix = $this->get_warning_suffix();
293
294 $warning = $warning_prefix . $warning_core_message . $browser_extension_message . $warning_suffix;
295
296 $browser_message = $warning_prefix . $browser_extension_message . $warning_suffix;
297
298 return array(
299 'warning' => $warning,
300 'all_server_requirement_passed' => $all_server_requirement_passed,
301 'browser_message' => $browser_message,
302 );
303 }
304
305 /**
306 * Get list of failed extensions.
307 *
308 * @return array Array of failed extensions or empty array if all extensions exists.
309 */
310 private function get_failed_extension(): array {
311 $failed_extensions = array();
312
313 if ( ! $this->is_enabled_gmp() ) {
314 $failed_extensions[] = 'GMP';
315 }
316
317 if ( ! $this->is_enabled_mbstring() ) {
318 $failed_extensions[] = 'MBSTRING';
319 }
320
321 if ( ! $this->is_enabled_sodium() ) {
322 $failed_extensions[] = 'SODIUM';
323 }
324
325 return $failed_extensions;
326 }
327
328 /**
329 * Concatenate multiple failed extensions.
330 * Form a comma separated with and before last element string from array of elements, if elements is more than two.
331 *
332 * @param array $extensions list of failed extensions.
333 *
334 * @return string Return concatenated string on success else empty string.
335 */
336 private function concatenate_failed_extensions( array $extensions ): string {
337 if ( count( $extensions ) === 1 ) {
338 return $extensions[0];
339 }
340
341 if ( count( $extensions ) > 1 ) {
342 $last_extension = esc_html__( ' and ', 'defender-security' ) . array_pop( $extensions );
343
344 return implode( ', ', $extensions ) . $last_extension;
345 }
346
347 return '';
348 }
349
350 /**
351 * Create extension warning message
352 *
353 * @return string Warning message if extension not met the requirement or empty string for met requirement.
354 */
355 private function get_extension_warning_message(): string {
356 $message = '';
357 $failed_extensions = $this->get_failed_extension();
358 $failed_extensions_count = count( $failed_extensions );
359
360 if ( $failed_extensions_count > 0 ) {
361 $extension_string = $this->concatenate_failed_extensions( $failed_extensions );
362
363 $message .= sprintf(
364 /* translators: %s: Missing PHP extension(s) */
365 _n(
366 'The <strong>%s extension</strong> is not active on your server. ',
367 'The <strong>%s extensions</strong> are not active on your server. ',
368 $failed_extensions_count,
369 'defender-security'
370 ),
371 $extension_string
372 );
373 }
374
375 return $message;
376 }
377
378 /**
379 * Retrieves the browser name and optionally the version.
380 *
381 * @param bool $with_version Whether to include the version number.
382 *
383 * @return string Browser name, optionally including the version.
384 */
385 private function get_browser_name( $with_version = true ): string {
386 if ( ! function_exists( 'wp_check_browser_version' ) ) {
387 include_once ABSPATH . 'wp-admin/includes/dashboard.php';
388 }
389
390 $browser = wp_check_browser_version();
391 $browser_string = $browser['name'] . ( $with_version ? ' ' . $browser['version'] : '' );
392
393 return $browser_string;
394 }
395
396 /**
397 * Determine the warning suffix.
398 *
399 * @return string Warning suffix sentence.
400 */
401 private function get_warning_suffix(): string {
402 $warning_suffix = '';
403 if ( ( new WPMUDEV() )->show_support_links() ) {
404 $warning_suffix = defender_support_ticket_text();
405 }
406
407 return $warning_suffix;
408 }
409
410 /**
411 * Check if OTP screen should be shown to user.
412 *
413 * @param WP_User $user The user to check.
414 *
415 * @return bool
416 * @since 3.3.0
417 */
418 public function is_otp_screen_available( WP_User $user ): bool {
419 $is_available = false;
420 $service_two_fa = wd_di()->get( Two_Fa::class );
421 $enabled_providers = $service_two_fa->get_enabled_providers_for_user( $user );
422
423 if ( isset( $user->ID ) && $user->ID > 0 && true === in_array( self::$slug, $enabled_providers, true ) ) {
424 $controller = wd_di()->get( Webauthn_Controller::class );
425 $service_webauthn = wd_di()->get( Webauthn_Component::class );
426 $user_entity = $controller->get_user_entity( $user->ID );
427
428 if ( false !== $user_entity ) {
429 $credential_sources = $service_webauthn->findAllForUserEntity( $user_entity );
430
431 if ( is_array( $credential_sources ) && 0 < count( $credential_sources ) ) {
432 $is_available = true;
433 }
434 }
435 }
436
437 return $is_available;
438 }
439 }
440