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

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

410 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Provider for two-factor authentication using backup codes.
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_Factor\Two_Factor_Provider;
13
14 /**
15 * Manage backup codes for two-factor authentication.
16 *
17 * @since 2.8.0
18 */
19 class Backup_Codes extends Two_Factor_Provider {
20
21 /**
22 * 2fa provider slug.
23 *
24 * @var string
25 */
26 public static $slug = 'backup-codes';
27
28 /**
29 * The number backup codes.
30 *
31 * @var int
32 */
33 public const BACKUP_CODE_COUNT = 5;
34
35 /**
36 * The length of each backup code.
37 *
38 * @var int
39 */
40 public const BACKUP_CODE_SIZE = 8;
41
42 /**
43 * Characters used to generate the backup codes.
44 *
45 * @var string
46 */
47 public const BACKUP_CODE_CHARACTERS = '1234567890';
48
49 /**
50 * The user meta key for backup codes.
51 *
52 * @var string
53 */
54 public const BACKUP_CODE_VALUES = 'wd_2fa_backup_codes';
55
56 /**
57 * Meta key to check if backup codes are activated for the user.
58 *
59 * @var string
60 */
61 public const BACKUP_CODE_START = 'wd_2fa_backup_codes_is_activated';
62
63 /**
64 * Indicates whether backup codes are activated for the user.
65 *
66 * @var string
67 */
68 protected $is_activated;
69
70 /**
71 * Description of the backup codes' provider.
72 *
73 * @var string
74 */
75 protected $description;
76
77 /**
78 * Constructor that initializes actions.
79 */
80 public function __construct() {
81 add_action( 'admin_notices', array( $this, 'admin_notices' ) );
82 add_action( 'wd_2fa_init_provider_' . self::$slug, array( $this, 'init_provider' ) );
83 add_action( 'wd_2fa_user_options_' . self::$slug, array( $this, 'user_options' ) );
84 }
85
86 /**
87 * Initializes the provider for a user.
88 *
89 * @param WP_User $user The user object.
90 */
91 public function init_provider( WP_User $user ): void {
92 $this->is_activated = get_user_meta( $user->ID, self::BACKUP_CODE_START, true );
93 $this->description = empty( $this->is_activated )
94 ? esc_html__( 'Generate non-expirable backup codes that can be used to log in once.', 'defender-security' )
95 : esc_html__( 'Each backup code can only be used to log in once.', 'defender-security' );
96 }
97
98 /**
99 * Get the name of the provider.
100 *
101 * @return string
102 */
103 public function get_label(): string {
104 return esc_html__( 'Backup Codes', 'defender-security' );
105 }
106
107 /**
108 * Returns the label for the backup codes' provider.
109 *
110 * @return string The label.
111 */
112 public function get_login_label(): string {
113 return $this->get_label();
114 }
115
116 /**
117 * Returns the login label for the backup codes' provider.
118 *
119 * @return string The login label.
120 */
121 public function get_user_label(): string {
122 return esc_html__( 'Backup codes', 'defender-security' );
123 }
124
125 /**
126 * Get the desc of the provider.
127 *
128 * @return string
129 */
130 public function get_description(): string {
131 return $this->description;
132 }
133
134 /**
135 * Whether this 2FA provider is configured and codes are available for the user specified.
136 *
137 * @param WP_User $user The user object.
138 *
139 * @return bool
140 */
141 public function is_available_for_user( WP_User $user ): bool {
142 // Does this user have available codes?
143 if ( 0 < self::get_unused_codes_for_user( $user ) ) {
144 return true;
145 }
146
147 return false;
148 }
149
150 /**
151 * Displays the authentication form for backup codes.
152 */
153 public function authentication_form(): void {
154 ?>
155 <p class="wpdef-2fa-label"><?php echo $this->get_login_label(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
156 <p class="wpdef-2fa-text">
157 <?php echo esc_html__( 'Enter one of your recovery codes to log in to your account.', 'defender-security' ); ?>
158 </p>
159 <input type="text" autofocus value="" autocomplete="off" name="backup-codes">
160 <button class="button button-primary float-r"
161 type="submit"><?php esc_attr_e( 'Authenticate', 'defender-security' ); ?></button>
162 <?php
163 }
164
165 /**
166 * Return the number of unused codes for the specified user.
167 *
168 * @param WP_User $user The user object.
169 *
170 * @return int
171 */
172 public static function get_unused_codes_for_user( WP_User $user ): int {
173 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
174 if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
175 return count( $backup_codes );
176 }
177
178 return 0;
179 }
180
181 /**
182 * Generate a random string for an auth code.
183 *
184 * @return string
185 */
186 private static function get_code(): string {
187 $code = '';
188 $chars = self::BACKUP_CODE_CHARACTERS;
189 for ( $i = 0; $i < self::BACKUP_CODE_SIZE; $i++ ) {
190 $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
191 }
192
193 return $code;
194 }
195
196 /**
197 * Show an admin notice when backup codes have run out.
198 *
199 * @return void
200 */
201 public function admin_notices(): void {
202 $user = wp_get_current_user();
203 if ( ! is_object( $user ) ) {
204 return;
205 }
206 // Is User role from common list checked?
207 if ( empty( array_intersect( $user->roles, $this->get_model()->user_roles ) ) ) {
208 return;
209 }
210 // Return if the provider is not enabled.
211 if ( ! in_array( self::$slug, $this->get_component()->get_enabled_providers_for_user( $user->ID ), true ) ) {
212 return;
213 }
214 // Return if we are not out of codes.
215 if ( $this->is_available_for_user( $user ) ) {
216 return;
217 }
218 ?>
219 <div class="error">
220 <p>
221 <span>
222 <?php
223 printf(
224 /* translators: %s: URL to regenerate code */
225 esc_html__(
226 'You\'ve used all generated backup codes. Please %s new codes accordingly.',
227 'defender-security'
228 ),
229 '<a href="' . esc_url( get_edit_user_link( $user->ID ) . '#wpdef-2fa-backup-codes' ) . '">' . esc_html__( 'generate', 'defender-security' ) . '</a>'
230 );
231 ?>
232 <span>
233 </p>
234 </div>
235 <?php
236 }
237
238 /**
239 * Display auth method.
240 *
241 * @param WP_User $user The user object.
242 *
243 * @return void
244 */
245 public function user_options( WP_User $user ): void {
246 $cond = $this->get_component()->is_checked_enabled_provider_by_slug( $user, self::$slug )
247 && $this->is_available_for_user( $user );
248 $attr = $cond ? '' : ' disabled';
249 // Check whether codes were generated before or not.
250 if ( empty( $this->is_activated ) ) {
251 $button_text = esc_html__( 'Generate Backup Codes', 'defender-security' );
252 $class = 'hidden';
253 $count = 0;
254 $show_notice = 'no';
255 } else {
256 $button_text = esc_html__( 'Get New Codes', 'defender-security' );
257 $class = $cond ? '' : 'hidden';
258 $count = self::get_unused_codes_for_user( $user );
259 $show_notice = 0 === $count ? 'yes' : 'no';
260 }
261
262 $filename = str_replace(
263 array(
264 'http://www.',
265 'http://',
266 'https://www.',
267 'https://',
268 ),
269 '',
270 get_bloginfo( 'url' )
271 );
272 $filename = str_replace( '/', '-', $filename );
273 $filename = sanitize_file_name( $filename . '-backup-codes.txt' );
274
275 $this->get_controller()->render_partial(
276 'two-fa/providers/backup-codes',
277 array(
278 'number_of_codes' => self::display_number_of_codes( $count ),
279 'url' => $this->get_url( 'generate_backup_codes' ),
280 'user' => $user,
281 'attr' => $attr,
282 'class' => $class,
283 'show_notice' => $show_notice,
284 'button_text' => $button_text,
285 'filename' => $filename,
286 )
287 );
288 }
289
290 /**
291 * Generate backup codes.
292 *
293 * @param WP_User $user The user object.
294 * @param array $args Optional arguments to customize code generation.
295 *
296 * @return array
297 */
298 public static function generate_codes( WP_User $user, array $args = array() ): array {
299 $codes = array();
300 $codes_hashed = array();
301 // Check for arguments.
302 if ( isset( $args['number'] ) ) {
303 $num_codes = (int) $args['number'];
304 } else {
305 $num_codes = self::BACKUP_CODE_COUNT;
306 }
307 // Add a flag if it haven't existed yet.
308 if ( empty( get_user_meta( $user->ID, self::BACKUP_CODE_START, true ) ) ) {
309 update_user_meta( $user->ID, self::BACKUP_CODE_START, 1 );
310 }
311 if ( isset( $args['method'] ) && 'append' === $args['method'] ) {
312 $codes_hashed = (array) get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
313 }
314
315 for ( $i = 0; $i < $num_codes; $i++ ) {
316 $code = self::get_code();
317 $codes_hashed[] = wp_hash_password( $code );
318 $codes[] = $code;
319 unset( $code );
320 }
321
322 update_user_meta( $user->ID, self::BACKUP_CODE_VALUES, $codes_hashed );
323
324 // Unhashed values.
325 return $codes;
326 }
327
328 /**
329 * Delete codes.
330 *
331 * @param WP_User $user The user object.
332 * @param string $code_hashed The hashed code to delete.
333 *
334 * @return void
335 */
336 public function delete_code( WP_User $user, string $code_hashed ): void {
337 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
338 // Delete the current code from the list since it's been used.
339 $backup_codes = array_flip( $backup_codes );
340 unset( $backup_codes[ $code_hashed ] );
341 $backup_codes = array_values( array_flip( $backup_codes ) );
342 // Update the backup code master list.
343 update_user_meta( $user->ID, self::BACKUP_CODE_VALUES, $backup_codes );
344 }
345
346 /**
347 * Displays the number of unused codes.
348 *
349 * @param int $count The number of unused codes.
350 *
351 * @return string
352 */
353 public static function display_number_of_codes( int $count ): string {
354 return sprintf(
355 /* translators: %s: count */
356 _n( '%s unused code remaining', '%s unused codes remaining', $count, 'defender-security' ),
357 $count
358 );
359 }
360
361 /**
362 * Validates a backup code entered by the user.
363 *
364 * @param WP_User $user The user object.
365 * @param string $code The backup code to validate.
366 *
367 * @return bool|WP_Error
368 */
369 protected function validate_code( WP_User $user, string $code ) {
370 $backup_codes = get_user_meta( $user->ID, self::BACKUP_CODE_VALUES, true );
371
372 if ( is_array( $backup_codes ) && ! empty( $backup_codes ) ) {
373 foreach ( $backup_codes as $code_hashed ) {
374 if ( wp_check_password( $code, $code_hashed, $user->ID ) ) {
375 $this->delete_code( $user, $code_hashed );
376
377 return true;
378 }
379 }
380 }
381
382 $lockout_message = $this->get_component()->verify_attempt( $user->ID, self::$slug );
383
384 return new WP_Error(
385 'opt_fail',
386 empty( $lockout_message ) ? esc_html__( 'ERROR: Invalid verification code.', 'defender-security' ) : $lockout_message
387 );
388 }
389
390 /**
391 * Validates the authentication for a user using a backup code.
392 *
393 * @param WP_User $user The user object.
394 *
395 * @return bool|WP_Error True if authenticated, WP_Error if not.
396 */
397 public function validate_authentication( WP_User $user ) {
398 $codes = defender_get_data_from_request( 'backup-codes', 'p' ) ?? false;
399
400 if ( false === $codes ) {
401 return new WP_Error(
402 'opt_fail',
403 esc_html__( 'ERROR: Invalid backup code.', 'defender-security' )
404 );
405 }
406
407 return $this->validate_code( $user, $codes );
408 }
409 }
410