PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 1.5.2
WP 2FA – Two-factor authentication for WordPress v1.5.2
1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Authenticator / Authentication.php
wp-2fa / includes / classes / Authenticator Last commit date
Authentication.php 5 years ago BackupCodes.php 6 years ago Login.php 5 years ago
Authentication.php
592 lines
1 <?php // phpcs:ignore
2 /**
3 * Class for handling general authentication tasks.
4 *
5 * @since 0.1-dev
6 *
7 * @package WP2FA
8 */
9
10 namespace WP2FA\Authenticator;
11
12 use WP2FA\Admin\SettingsPage;
13 use \WP2FA\WP2FA as WP2FA;
14
15 /**
16 * Authenticator class
17 */
18 class Authentication {
19
20 const SECRET_META_KEY = 'wp_2fa_totp_key';
21 const NOTICES_META_KEY = 'wp_2fa_totp_notices';
22 const TOKEN_META_KEY = 'wp_2fa_email_token';
23 const DEFAULT_KEY_BIT_SIZE = 160;
24 const DEFAULT_CRYPTO = 'sha1';
25 const DEFAULT_DIGIT_COUNT = 6;
26 const DEFAULT_TIME_STEP_SEC = 30;
27 const DEFAULT_TIME_STEP_ALLOWANCE = 4;
28 private static $_base_32_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
29
30 /**
31 * Constructor.
32 */
33 public function __construct() {
34
35 }
36
37 /**
38 * Gemerate QR code
39 *
40 * @param string $name Username.
41 * @param string $key Auth key.
42 * @param string $title Site title.
43 * @return string QR code URL.
44 */
45 public static function get_google_qr_code( $name, $key, $title = null ) {
46 // Encode to support spaces, question marks and other characters.
47 $name = rawurlencode( $name );
48 $google_url = urlencode( 'otpauth://totp/' . $name . '?secret=' . $key );
49 if ( isset( $title ) ) {
50 $google_url .= urlencode( '&issuer=' . rawurlencode( $title ) );
51 }
52 return 'https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=' . $google_url;
53 }
54
55 /**
56 * Generates key
57 *
58 * @param int $bitsize Nume of bits to use for key.
59 *
60 * @return string $bitsize long string composed of available base32 chars.
61 */
62 public static function generate_key( $bitsize = self::DEFAULT_KEY_BIT_SIZE ) {
63 $bytes = ceil( $bitsize / 8 );
64 $secret = wp_generate_password( $bytes, true, true );
65
66 return self::base32_encode( $secret );
67 }
68 /**
69 * Returns a base32 encoded string.
70 *
71 * @param string $string String to be encoded using base32.
72 *
73 * @return string base32 encoded string without padding.
74 */
75 public static function base32_encode( $string ) {
76 if ( empty( $string ) ) {
77 return '';
78 }
79
80 $binary_string = '';
81
82 foreach ( str_split( $string ) as $character ) {
83 $binary_string .= str_pad( base_convert( ord( $character ), 10, 2 ), 8, '0', STR_PAD_LEFT );
84 }
85
86 $five_bit_sections = str_split( $binary_string, 5 );
87 $base32_string = '';
88
89 foreach ( $five_bit_sections as $five_bit_section ) {
90 $base32_string .= self::$_base_32_chars[ base_convert( str_pad( $five_bit_section, 5, '0' ), 2, 10 ) ];
91 }
92
93 return $base32_string;
94 }
95
96 /**
97 * Get the TOTP secret key for a user.
98 *
99 * @param int $user_id User ID.
100 *
101 * @return string
102 */
103 public static function get_user_totp_key( $user_id ) {
104 return (string) get_user_meta( $user_id, self::SECRET_META_KEY, true );
105 }
106
107 /**
108 * Check if the TOTP secret key has a proper format.
109 *
110 * @param string $key TOTP secret key.
111 *
112 * @return boolean
113 */
114 public static function is_valid_key( $key ) {
115 $check = sprintf( '/^[%s]+$/', self::$_base_32_chars );
116
117 if ( 1 === preg_match( $check, $key ) ) {
118 return true;
119 }
120
121 return false;
122 }
123
124 /**
125 * Checks if a given code is valid for a given key, allowing for a certain amount of time drift
126 *
127 * @param string $key The share secret key to use.
128 * @param string $authcode The code to test.
129 *
130 * @return bool Whether the code is valid within the time frame
131 */
132 public static function is_valid_authcode( $key, $authcode ) {
133
134 $max_ticks = apply_filters( 'wp_2fa_totp_time_step_allowance', self::DEFAULT_TIME_STEP_ALLOWANCE );
135
136 // Array of all ticks to allow, sorted using absolute value to test closest match first.
137 $ticks = range( - $max_ticks, $max_ticks );
138 usort( $ticks, array( __CLASS__, 'abssort' ) );
139
140 $time = time() / self::DEFAULT_TIME_STEP_SEC;
141 foreach ( $ticks as $offset ) {
142 $log_time = $time + $offset;
143 $calculdated = (string) self::calc_totp( $key, $log_time );
144 if ( $calculdated === $authcode ) {
145 return true;
146 }
147 }
148 return false;
149 }
150
151 /**
152 * Calculate a valid code given the shared secret key
153 *
154 * @param string $key The shared secret key to use for calculating code.
155 * @param mixed $step_count The time step used to calculate the code, which is the floor of time() divided by step size.
156 * @param int $digits The number of digits in the returned code.
157 * @param string $hash The hash used to calculate the code.
158 * @param int $time_step The size of the time step.
159 *
160 * @return string The totp code
161 */
162 public static function calc_totp( $key, $step_count = false, $digits = self::DEFAULT_DIGIT_COUNT, $hash = self::DEFAULT_CRYPTO, $time_step = self::DEFAULT_TIME_STEP_SEC ) {
163
164 $secret = self::base32_decode( $key );
165
166 if ( false === $step_count ) {
167 $step_count = floor( time() / $time_step );
168 }
169
170 $timestamp = self::pack64( $step_count );
171
172 $hash = hash_hmac( $hash, $timestamp, $secret, true );
173
174 $offset = ord( $hash[19] ) & 0xf;
175
176 $code = (
177 ( ( ord( $hash[ $offset + 0 ] ) & 0x7f ) << 24 ) |
178 ( ( ord( $hash[ $offset + 1 ] ) & 0xff ) << 16 ) |
179 ( ( ord( $hash[ $offset + 2 ] ) & 0xff ) << 8 ) |
180 ( ord( $hash[ $offset + 3 ] ) & 0xff )
181 ) % pow( 10, $digits );
182
183 return str_pad( $code, $digits, '0', STR_PAD_LEFT );
184 }
185
186 /**
187 * Decode a base32 string and return a binary representation
188 *
189 * @param string $base32_string The base 32 string to decode.
190 *
191 * @throws Exception If string contains non-base32 characters.
192 *
193 * @return string Binary representation of decoded string
194 */
195 public static function base32_decode( $base32_string ) {
196
197 $base32_string = strtoupper( $base32_string );
198
199 if ( ! preg_match( '/^[' . self::$_base_32_chars . ']+$/', $base32_string, $match ) ) {
200 throw new \Exception( 'Invalid characters in the base32 string.' );
201 }
202
203 $l = strlen( $base32_string );
204 $n = 0;
205 $j = 0;
206 $binary = '';
207
208 for ( $i = 0; $i < $l; $i++ ) {
209
210 $n = $n << 5; // Move buffer left by 5 to make room.
211 $n = $n + strpos( self::$_base_32_chars, $base32_string[ $i ] ); // Add value into buffer.
212 $j += 5; // Keep track of number of bits in buffer.
213
214 if ( $j >= 8 ) {
215 $j -= 8;
216 $binary .= chr( ( $n & ( 0xFF << $j ) ) >> $j );
217 }
218 }
219
220 return $binary;
221 }
222
223 /**
224 * Used with usort to sort an array by distance from 0
225 *
226 * @param int $a First array element.
227 * @param int $b Second array element.
228 *
229 * @return int -1, 0, or 1 as needed by usort
230 */
231 private static function abssort( $a, $b ) {
232 $a = abs( $a );
233 $b = abs( $b );
234 if ( $a === $b ) {
235 return 0;
236 }
237 return ( $a < $b ) ? -1 : 1;
238 }
239
240 /**
241 * Pack stuff
242 *
243 * @param string $value The value to be packed.
244 *
245 * @return string Binary packed string.
246 */
247 public static function pack64( $value ) {
248 // 64bit mode (PHP_INT_SIZE == 8).
249 if ( PHP_INT_SIZE >= 8 ) {
250 // If we're on PHP 5.6.3+ we can use the new 64bit pack functionality.
251 if ( version_compare( PHP_VERSION, '5.6.3', '>=' ) && PHP_INT_SIZE >= 8 ) {
252 return pack( 'J', $value );
253 }
254 $highmap = 0xffffffff << 32;
255 $higher = ( $value & $highmap ) >> 32;
256 } else {
257 /*
258 * 32bit PHP can't shift 32 bits like that, so we have to assume 0 for the higher
259 * and not pack anything beyond it's limits.
260 */
261 $higher = 0;
262 }
263
264 $lowmap = 0xffffffff;
265 $lower = $value & $lowmap;
266
267 return pack( 'NN', $higher, $lower );
268 }
269
270 /**
271 * Generate a random eight-digit string to send out as an auth code.
272 *
273 * @since 0.1-dev
274 *
275 * @param int $length The code length.
276 * @param string|array $chars Valid auth code characters.
277 * @return string
278 */
279 public static function get_code( $length = 8, $chars = '1234567890' ) {
280 $code = '';
281 if ( is_array( $chars ) ) {
282 $chars = implode( '', $chars );
283 }
284 for ( $i = 0; $i < $length; $i++ ) {
285 $code .= substr( $chars, wp_rand( 0, strlen( $chars ) - 1 ), 1 );
286 }
287 return $code;
288 }
289
290 /**
291 * Generate the user token.
292 *
293 * @since 0.1-dev
294 *
295 * @param int $user_id User ID.
296 * @return string
297 */
298 public static function generate_token( $user_id ) {
299 $token = self::get_code();
300 update_user_meta( $user_id, self::TOKEN_META_KEY, wp_hash( $token ) );
301 return $token;
302 }
303
304 /**
305 * Validate the user token.
306 *
307 * @since 0.1-dev
308 *
309 * @param int $user_id User ID.
310 * @param string $token User token.
311 * @return boolean
312 */
313 public static function validate_token( $user_id, $token ) {
314 $hashed_token = self::get_user_token( $user_id );
315 // Bail if token is empty or it doesn't match.
316 if ( empty( $hashed_token ) || ( wp_hash( $token ) !== $hashed_token ) ) {
317 return false;
318 }
319
320 // Ensure that the token can't be re-used.
321 self::delete_token( $user_id );
322
323 return true;
324 }
325
326 /**
327 * Delete the user token.
328 *
329 * @since 0.1-dev
330 *
331 * @param int $user_id User ID.
332 */
333 public static function delete_token( $user_id ) {
334 delete_user_meta( $user_id, self::TOKEN_META_KEY );
335 }
336
337 /**
338 * Check if user has a valid token already.
339 *
340 * @param int $user_id User ID.
341 * @return boolean If user has a valid email token.
342 */
343 public static function user_has_token( $user_id ) {
344 $hashed_token = self::get_user_token( $user_id );
345 if ( ! empty( $hashed_token ) ) {
346 return true;
347 } else {
348 return false;
349 }
350 }
351
352 /**
353 * Get the authentication token for the user.
354 *
355 * @param int $user_id User ID.
356 *
357 * @return string|boolean User token or `false` if no token found.
358 */
359 public static function get_user_token( $user_id ) {
360 $hashed_token = get_user_meta( $user_id, self::TOKEN_META_KEY, true );
361
362 if ( ! empty( $hashed_token ) && is_string( $hashed_token ) ) {
363 return $hashed_token;
364 }
365
366 return false;
367 }
368
369 /**
370 * Delete the TOTP secret key for a user.
371 *
372 * @param int $user_id User ID.
373 *
374 * @return boolean If the key was deleted successfully.
375 */
376 public static function delete_user_totp_key( $user_id ) {
377 return delete_user_meta( $user_id, self::SECRET_META_KEY );
378 }
379
380 /**
381 * Is user eligible for 2FA.
382 *
383 * @param int $user_id User id.
384 * @param string $current_policy Specific policy to check against.
385 */
386 public static function is_user_eligible_for_2fa( $user_id, $current_policy = '', $excluded_users = '', $excluded_roles = '', $enforced_users = '', $enforced_roles = '' ) {
387 if ( isset( $_GET['user_id'] ) ) {
388 $user_id = (int) $_GET['user_id'];
389 $user = get_user_by( 'id', $user_id );
390 $user_roles = $user->roles;
391 } elseif ( isset( $user_id ) ) {
392 $user = get_user_by( 'id', $user_id );
393 $user_roles = $user->roles;
394 } else {
395 $user = wp_get_current_user();
396 $user_roles = $user->roles;
397 }
398
399 if ( $current_policy ) {
400 $current_policy = $current_policy;
401 } else {
402 $current_policy = WP2FA::get_wp2fa_setting( 'enforcement-policy' );
403 }
404
405 $enabled_method = get_user_meta( $user->ID, 'wp_2fa_enabled_methods', true );
406 $user_eligable = false;
407
408 // Lets check the policy settings and if the user has setup totp/email by checking for the usermeta.
409 if ( empty( $enabled_method ) && WP2FA::is_this_multisite() && 'superadmins-only' === $current_policy ) {
410 return is_super_admin( $user->ID );
411 } else if ( 'all-users' === $current_policy && empty( $enabled_method ) ) {
412
413 if ( isset( $excluded_users ) ) {
414 $excluded_users = $excluded_users;
415 } else {
416 $excluded_users = WP2FA::get_wp2fa_setting( 'excluded_users' );
417 }
418
419 if ( ! empty( $excluded_users ) ) {
420 // Turn it into an array.
421 $excluded_users_array = explode( ',', $excluded_users );
422 // Compare our roles with the users and see if we get a match.
423 $result = in_array( $user->user_login, $excluded_users_array, true );
424 if ( ! $result ) {
425 $user_eligable = true;
426 }
427 }
428
429 if ( isset( $excluded_roles ) ) {
430 $excluded_roles = $excluded_roles;
431 } else {
432 $excluded_roles = WP2FA::get_wp2fa_setting( 'excluded_roles' );
433 }
434
435 if ( ! empty( $excluded_roles ) ) {
436 // Turn it into an array.
437 $excluded_roles_array = explode( ',', strtolower( $excluded_roles ) );
438 // Compare our roles with the users and see if we get a match.
439 $result = array_intersect( $excluded_roles_array, $user->roles );
440
441 if ( ! empty( $result ) ) {
442 $user_eligable = true;
443 }
444
445 if ( WP2FA::is_this_multisite() ) {
446 $users_caps = array();
447 $subsites = get_sites();
448 // Check each site and add to our array so we know each users actual roles.
449 foreach ( $subsites as $subsite ) {
450 $subsite_id = get_object_vars( $subsite )['blog_id'];
451 $users_caps[] = get_user_meta( $user->ID, 'wp_' .$subsite_id .'_capabilities', true );
452 }
453 // Strip the top layer ready.
454 $users_caps = $users_caps;
455 foreach ( $users_caps as $key => $value ) {
456 if ( ! empty( $value ) ) {
457 foreach ( $value as $key => $value ) {
458 $result = in_array( $key, $excluded_roles_array, true );
459 }
460 }
461 }
462 if ( ! empty( $result ) ) {
463 return false;
464 }
465 }
466 }
467
468 if ( true === $user_eligable || empty( $enabled_method ) ) {
469 return true;
470 }
471 } elseif ( 'certain-roles-only' === $current_policy && empty( $enabled_method ) ) {
472
473 if ( isset( $enforced_users ) && ! empty( $enforced_users ) ) {
474 $enforced_users = $enforced_users;
475 } else {
476 $enforced_users = WP2FA::get_wp2fa_setting( 'enforced_users' );
477 }
478
479 if ( ! empty( $enforced_users )) {
480 // Turn it into an array.
481 $enforced_users_array = explode( ',', $enforced_users );
482 // Compare our roles with the users and see if we get a match.
483 $result = in_array( $user->user_login, $enforced_users_array, true );
484 // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag.
485 if ( ! empty( $result ) ) {
486 return true;
487 }
488 }
489
490 if ( isset( $enforced_roles ) && ! empty( $enforced_roles ) ) {
491 $enforced_roles = $enforced_roles;
492 } else {
493 $enforced_roles = WP2FA::get_wp2fa_setting( 'enforced_roles' );
494 }
495
496 if ( ! empty( $enforced_roles ) ) {
497 // Turn it into an array.
498 $enforced_roles_array = SettingsPage::extract_roles_from_input( $enforced_roles );
499 // Compare our roles with the users and see if we get a match.
500 $result = array_intersect( $enforced_roles_array, $user->roles );
501 // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag.
502 if ( ! empty( $result ) ) {
503 return true;
504 }
505
506 if ( WP2FA::is_this_multisite() ) {
507 $users_caps = array();
508 $subsites = get_sites();
509 // Check each site and add to our array so we know each users actual roles.
510 foreach ( $subsites as $subsite ) {
511 $subsite_id = get_object_vars( $subsite )['blog_id'];
512 $users_caps[] = get_user_meta( $user->ID, 'wp_' .$subsite_id .'_capabilities', true );
513 }
514 // Strip the top layer ready.
515 $users_caps = $users_caps;
516 foreach ( $users_caps as $key => $value ) {
517 if ( ! empty( $value ) ) {
518 foreach ( $value as $key => $value ) {
519 $result = in_array( $key, $enforced_roles_array, true );
520 }
521 }
522 }
523 if ( ! empty( $result ) ) {
524 return true;
525 }
526 }
527 }
528
529 } elseif ( 'certain-users-only' === $current_policy && empty( $enabled_method ) ) {
530
531 if ( isset( $enforced_users ) && ! empty( $enforced_users ) ) {
532 $enforced_users = $enforced_users;
533 } else {
534 $enforced_users = WP2FA::get_wp2fa_setting( 'enforced_users' );
535 }
536
537 if ( ! empty( $enforced_users ) ) {
538 // Turn it into an array.
539 $enforced_users_array = explode( ',', $enforced_users );
540 // Compare our roles with the users and see if we get a match.
541 $result = in_array( $user->user_login, $enforced_users_array, true );
542 // The user is one of the chosen roles we are forcing 2FA onto, so lets show the nag.
543 if ( ! empty( $result ) ) {
544 return true;
545 }
546 }
547 }
548
549 return false;
550 }
551
552 public static function getApps() {
553 return [
554 'authy' => [
555 'logo' => 'authy-logo.png',
556 'hash' => 'authy',
557 'name' => 'Authy'
558 ],
559 'google' => [
560 'logo' => 'google-logo.png',
561 'hash' => 'google',
562 'name' => 'Google Authenticator'
563 ],
564 'microsoft' => [
565 'logo' => 'microsoft-logo.png',
566 'hash' => 'microsoft',
567 'name' => 'Microsoft Authenticator'
568 ],
569 'duo' => [
570 'logo' => 'duo-logo.png',
571 'hash' => 'duo',
572 'name' => 'Duo Security'
573 ],
574 'lastpass' => [
575 'logo' => 'lastpass-logo.png',
576 'hash' => 'lastpass',
577 'name' => 'LastPass'
578 ],
579 'freeotp' => [
580 'logo' => 'free-otp-logo.png',
581 'hash' => 'freeotp',
582 'name' => 'FreeOTP'
583 ],
584 'okta' => [
585 'logo' => 'okta-logo.png',
586 'hash' => 'okta',
587 'name' => 'Okta'
588 ]
589 ];
590 }
591 }
592