PluginProbe
Two Factor Authentication / 1.14.3
Two Factor Authentication v1.14.3
1.12.2 1.13.0 1.14.10 1.14.11 1.14.14 1.14.15 1.14.16 1.14.17 1.14.23 1.14.24 1.14.26 1.14.27 1.14.3 1.14.4 1.14.5 1.14.7 1.14.8 1.15.5 1.16.0 1.2.10 1.2.12 1.2.13 1.2.14 1.2.15 1.2.16 All 98 releases
two-factor-authentication / simba-tfa / simba-tfa.php

simba-tfa.php in Two Factor Authentication 1.14.3, at simba-tfa/simba-tfa.php

1,081 lines 39.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) die('Access denied.');
4
5 class Simba_Two_Factor_Authentication {
6
7 protected $frontend;
8
9 protected $totp_controller;
10
11 /**
12 * Constructor, run upon plugin initiation
13 */
14 public function __construct() {
15
16 require_once(__DIR__.'/providers/totp-hotp/loader.php');
17
18 $this->totp_controller = new Simba_TFA_Provider_TOTP($this);
19
20 if (file_exists(__DIR__.'/premium/loader.php')) {
21 if (!class_exists('Simba_Two_Factor_Authentication_Premium')) include_once(__DIR__.'/premium/loader.php');
22 $GLOBALS['simba_two_factor_authentication_premium'] = new Simba_Two_Factor_Authentication_Premium($this);
23 }
24
25 // Process login form AJAX events
26 add_action('wp_ajax_nopriv_simbatfa-init-otp', array($this, 'tfaInitLogin'));
27 add_action('wp_ajax_simbatfa-init-otp', array($this, 'tfaInitLogin'));
28
29 add_action('wp_ajax_simbatfa_shared_ajax', array($this, 'shared_ajax'));
30
31 require_once($this->includes_dir().'/login-form-integrations.php');
32 new Simba_TFA_Login_Form_Integrations($this);
33
34 // Add TFA column on admin users list
35 add_action('manage_users_columns', array($this, 'manage_users_columns_tfa'));
36 add_action('wpmu_users_columns', array($this, 'manage_users_columns_tfa'));
37 add_action('manage_users_custom_column', array($this, 'manage_users_custom_column_tfa'), 10, 3);
38
39 // CSS for admin users screen
40 add_action('admin_print_styles-users.php', array($this, 'load_users_css'), 10, 0);
41
42 add_action('admin_init', array($this, 'register_two_factor_auth_settings'));
43 add_action('init', array($this, 'init'));
44
45 if (!defined('TWO_FACTOR_DISABLE') || !TWO_FACTOR_DISABLE) {
46 add_filter('authenticate', array($this, 'tfaVerifyCodeAndUser'), 99999999999, 3);
47 }
48
49 if (defined('DOING_AJAX') && DOING_AJAX && defined('WP_ADMIN') && WP_ADMIN && !empty($_REQUEST['action']) && 'simbatfa-init-otp' == $_REQUEST['action']) {
50 // Try to prevent PHP notices breaking the AJAX conversation
51 $this->output_buffering = true;
52 $this->logged = array();
53 set_error_handler(array($this, 'get_php_errors'), E_ALL & ~E_STRICT);
54 ob_start();
55 }
56 }
57
58 /**
59 * Give the filesystem path to the plugin's includes directory
60 *
61 * @return String
62 */
63 public function includes_dir() {
64 return __DIR__.'/includes';
65 }
66
67 /**
68 * Give the URL for the plugin's includes directory
69 *
70 * @return String
71 */
72 public function includes_url() {
73 return plugins_url('', __FILE__).'/includes';
74 }
75
76 /**
77 * Give the filesystem path to the plugin's templates directory
78 *
79 * @return String
80 */
81 public function templates_dir() {
82 return __DIR__.'/templates';
83 }
84
85 /**
86 * Give the filesystem path to the plugin's vendor directory
87 *
88 * @return String
89 */
90 public function vendor_dir() {
91 return __DIR__.'/vendor';
92 }
93
94 /**
95 * Include the user settings page code
96 */
97 public function show_dashboard_user_settings_page() {
98 $this->include_template('user-settings.php');
99 }
100
101 /**
102 * Enqueue CSS styling on the users page
103 */
104 public function load_users_css() {
105 wp_enqueue_style(
106 'tfa-users-css',
107 $this->includes_url().'/users.css',
108 array(),
109 $this->version,
110 'screen'
111 );
112 }
113
114 /**
115 * Add the 2FA label to the users list table header.
116 *
117 * @param Array $columns Table columns.
118 *
119 * @return Array
120 */
121 public function manage_users_columns_tfa($columns = array()) {
122 $columns['tfa-status'] = __('2FA', 'two-factor-authentication');
123 return $columns;
124 }
125
126 /**
127 * Add status into TFA column.
128 *
129 * @param String $value String.
130 * @param String $column_name Column name.
131 * @param Integer $user_id User ID.
132 *
133 * @return String
134 */
135 public function manage_users_custom_column_tfa($value = '', $column_name = '', $user_id = 0) {
136
137 // Only for this column name.
138 if ('tfa-status' === $column_name) {
139
140 if (!$this->is_activated_for_user($user_id)) {
141 $value = '&#8212;';
142 } elseif ($this->is_activated_by_user($user_id)) {
143 // Use value.
144 $value = '<span title="' . __( 'Enabled', 'two-factor-authentication' ) . '" class="dashicons dashicons-yes"></span>';
145 } else {
146 // No group.
147 $value = '<span title="' . __( 'Disabled', 'two-factor-authentication' ) . '" class="dashicons dashicons-no"></span>';
148 }
149 }
150
151 return $value;
152 }
153
154 /**
155 * Paint out an admin notice
156 *
157 * @param String $message - the caller should already have taken care of any escaping
158 * @param String $class
159 */
160 public function show_admin_warning($message, $class = 'updated') {
161 echo '<div class="tfamessage '.$class.'">'."<p>$message</p></div>";
162 }
163
164 /**
165 * Runs upon the WP action admin_init
166 */
167 public function register_two_factor_auth_settings() {
168 global $wp_roles;
169 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
170
171 foreach ($wp_roles->role_names as $id => $name) {
172 register_setting('tfa_user_roles_group', 'tfa_'.$id);
173 register_setting('tfa_user_roles_trusted_group', 'tfa_trusted_'.$id);
174 register_setting('tfa_user_roles_required_group', 'tfa_required_'.$id);
175 }
176
177 if (is_multisite()) {
178 register_setting('tfa_user_roles_group', 'tfa__super_admin');
179 register_setting('tfa_user_roles_trusted_group', 'tfa_trusted__super_admin');
180 register_setting('tfa_user_roles_required_group', 'tfa_required__super_admin');
181 }
182
183 register_setting('tfa_user_roles_required_group', 'tfa_requireafter');
184 register_setting('tfa_user_roles_required_group', 'tfa_if_required_redirect_to');
185 register_setting('tfa_user_roles_required_group', 'tfa_hide_turn_off');
186 register_setting('tfa_user_roles_trusted_group', 'tfa_trusted_for');
187 register_setting('simba_tfa_woocommerce_group', 'tfa_wc_add_section');
188 register_setting('simba_tfa_woocommerce_group', 'tfa_bot_protection');
189 register_setting('simba_tfa_default_hmac_group', 'tfa_default_hmac');
190 register_setting('tfa_xmlrpc_status_group', 'tfa_xmlrpc_on');
191 }
192
193 /**
194 * See whether TFA is available or not for a particular user - i.e. whether the administrator has permitted it for their user level
195 *
196 * @param Integer $user_id - WordPress user ID
197 *
198 * @return Boolean
199 */
200 public function is_activated_for_user($user_id) {
201
202 if (empty($user_id)) return false;
203
204 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
205 if (is_multisite() && is_super_admin($user_id)) {
206 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
207 $role = '_super_admin';
208 $db_val = $this->get_option('tfa_'.$role);
209 // Defaults to true if no setting has been saved
210 return (false === $db_val || $db_val) ? true : false;
211 }
212
213 $roles = $this->get_user_roles($user_id);
214
215 // N.B. This populates with roles on the current site within a multisite
216 foreach ($roles as $role) {
217 $db_val = $this->get_option('tfa_'.$role);
218 if (false === $db_val || $db_val) return true;
219 }
220
221 return false;
222
223 }
224
225 /**
226 * Get all user roles for a given user (if on multisite, amalgamates all roles from all sites)
227 *
228 * @param Integer $user_id - WordPress user ID
229 *
230 * @return Array
231 */
232 protected function get_user_roles($user_id) {
233
234 // Get roles on the main site
235 $user = new WP_User($user_id);
236 $roles = (array) $user->roles;
237
238 // On multisite, also check roles on non-main sites
239 if (is_multisite()) {
240 global $wpdb, $table_prefix;
241 $roles_db = $wpdb->get_results($wpdb->prepare("SELECT meta_key, meta_value FROM {$wpdb->usermeta} WHERE user_id=%d AND meta_key LIKE '".esc_sql($table_prefix)."%_capabilities'", $user_id));
242 if (is_array($roles_db)) {
243 foreach ($roles_db as $role_info) {
244 if (empty($role_info->meta_key) || !preg_match('/^'.$table_prefix.'\d+_capabilities$/', $role_info->meta_key) || empty($role_info->meta_value) || !preg_match('/^a:/', $role_info->meta_value)) continue;
245 $site_roles = unserialize($role_info->meta_value);
246 if (!is_array($site_roles)) continue;
247 foreach ($site_roles as $role => $active) {
248 if ($active && !in_array($role, $roles)) $roles[] = $role;
249 }
250 }
251 }
252 }
253
254 return $roles;
255 }
256
257 /**
258 * Check if TFA is required for a specified user
259 *
260 * N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
261 *
262 * @param $user_id Integer - the WP user ID
263 *
264 * @return Boolean
265 */
266 public function is_required_for_user($user_id) {
267 return apply_filters('simba_tfa_required_for_user', $this->user_property_active($user_id, 'required_'), $user_id);
268 }
269
270 /**
271 * See if a particular user property is active
272 *
273 * @param Integer $user_id
274 * @param String $prefix - e.g. "required_", "trusted_"
275 *
276 * @return Boolean
277 */
278 public function user_property_active($user_id, $prefix = 'required_') {
279
280 if (empty($user_id)) return false;
281
282 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
283 if (is_multisite() && is_super_admin($user_id)) {
284 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
285 $role = '_super_admin';
286 $db_val = $this->get_option('tfa_'.$prefix.$role);
287 return $db_val ? true : false;
288 }
289
290 $roles = $this->get_user_roles($user_id);
291
292 foreach ($roles as $role) {
293 $db_val = $this->get_option('tfa_'.$prefix.$role);
294 if ($db_val) return true;
295 }
296
297 return false;
298
299 }
300
301 /**
302 * Whether TFA is activated by a specific user. Note that this doesn't check if TFA is enabled for the user's role; the caller should check that first.
303 *
304 * @param Integer $user_id
305 *
306 * @return Boolean
307 */
308 public function is_activated_by_user($user_id) {
309 $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true);
310 return !empty($enabled);
311 }
312
313 /**
314 * Get a list of trusted devices for the user
315 *
316 * @param Integer|Boolean $user_id - WordPress user ID, or false for the current user
317 *
318 * @return Array
319 */
320 public function user_get_trusted_devices($user_id = false) {
321
322 if (false === $user_id) {
323 global $current_user;
324 $user_id = $current_user->ID;
325 }
326
327 $trusted_devices = get_user_meta($user_id, 'tfa_trusted_devices', true);
328
329 if (!is_array($trusted_devices)) $trusted_devices = array();
330
331 return $trusted_devices;
332 }
333
334 /**
335 * Trust the current device
336 *
337 * @param Integer $user_id - WordPress user ID
338 * @param Integer $trusted_for - time to trust for, in days
339 */
340 public function trust_device($user_id, $trusted_for) {
341
342 $trusted_devices = $this->user_get_trusted_devices($user_id);
343
344 $time_now = time();
345
346 foreach ($trusted_devices as $k => $device) {
347 if (empty($device['until']) || $device['until'] <= $time_now) unset($trusted_devices[$k]);
348 }
349
350 $until = $time_now + $trusted_for * 86400;
351
352 $token = bin2hex($this->random_bytes(40));
353
354 $trusted_devices[] = array(
355 'ip' => $_SERVER['REMOTE_ADDR'],
356 'until' => $until,
357 'user_agent' => empty($_SERVER['HTTP_USER_AGENT']) ? '' : (string) $_SERVER['HTTP_USER_AGENT'],
358 'token' => $token
359 );
360
361 $this->user_set_trusted_devices($user_id, $trusted_devices);
362
363 $this->set_cookie('simbatfa_trust_token', $token, $until);
364 }
365
366 /**
367 * Returns true if running on a PHP version on which mcrypt has been deprecated
368 *
369 * @return Boolean
370 */
371 public function is_mcrypt_deprecated() {
372 return (7 == PHP_MAJOR_VERSION && PHP_MINOR_VERSION >= 1);
373 }
374
375 /**
376 * Return the specified number of bytes
377 *
378 * @param Integer $bytes
379 *
380 * @throws Exception
381 *
382 * @return String
383 */
384 public function random_bytes($bytes) {
385 if (function_exists('random_bytes')) {
386 return random_bytes($bytes);
387 } elseif (function_exists('mcrypt_create_iv')) {
388 return $this->is_mcrypt_deprecated() ? @mcrypt_create_iv($bytes, MCRYPT_RAND) : mcrypt_create_iv($bytes, MCRYPT_RAND);
389 } elseif (function_exists('openssl_random_pseudo_bytes')) {
390 return openssl_random_pseudo_bytes($bytes);
391 }
392 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
393 }
394
395 /**
396 * Set a cookie so that, however we logged in, it can be found
397 *
398 * @param String $name - the cookie name
399 * @param String $value - the cookie value
400 * @param Integer $expires - when the cookie expires, in epoch time. Defaults to 24 hours' time. Values in the past cause cookie deletion.
401 */
402 protected function set_cookie($name, $value, $expires = null) {
403 if (null === $expires) $expires = time() + 86400;
404 $secure = is_ssl();
405 $secure_logged_in_cookie = ($secure && 'https' === parse_url(get_option('home'), PHP_URL_SCHEME));
406 $secure = apply_filters('secure_auth_cookie', $secure, get_current_user_id());
407 $secure_logged_in_cookie = apply_filters('secure_logged_in_cookie', $secure_logged_in_cookie, get_current_user_id(), $secure);
408
409 setcookie($name, $value, $expires, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, $secure, true);
410 setcookie($name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true);
411 if (COOKIEPATH != SITECOOKIEPATH) {
412 setcookie($name, $value, $expires, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true);
413 }
414 }
415
416 /**
417 * Get a list of trusted devices for the user
418 *
419 * @param Integer $user_id - WordPress user ID
420 * @param Array $trusted_devices - the list of devices
421 */
422 public function user_set_trusted_devices($user_id, $trusted_devices) {
423 update_user_meta($user_id, 'tfa_trusted_devices', $trusted_devices);
424 }
425
426 /**
427 * Get the user capability needed for managing TFA users.
428 * You'll want to think carefully about changing this to a non-admin, as it can give the ability to lock admins out (though, if you have FTP/files access, you can always disable TFA or any plugin)
429 *
430 * @return String
431 */
432 public function get_management_capability() {
433 return apply_filters('simba_tfa_management_capability', 'manage_options');
434 }
435
436 /**
437 * Used with set_error_handler()
438 *
439 * @param Integer $errno
440 * @param String $errstr
441 * @param String $errfile
442 * @param Integer $errline
443 *
444 * @return Boolean
445 */
446 public function get_php_errors($errno, $errstr, $errfile, $errline) {
447 if (0 == error_reporting()) return true;
448 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
449 $this->logged[] = $logline;
450 # Don't pass it up the chain (since it's going to be output to the user always)
451 return true;
452 }
453
454 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
455 switch ($errno) {
456 case 1: $e_type = 'E_ERROR'; break;
457 case 2: $e_type = 'E_WARNING'; break;
458 case 4: $e_type = 'E_PARSE'; break;
459 case 8: $e_type = 'E_NOTICE'; break;
460 case 16: $e_type = 'E_CORE_ERROR'; break;
461 case 32: $e_type = 'E_CORE_WARNING'; break;
462 case 64: $e_type = 'E_COMPILE_ERROR'; break;
463 case 128: $e_type = 'E_COMPILE_WARNING'; break;
464 case 256: $e_type = 'E_USER_ERROR'; break;
465 case 512: $e_type = 'E_USER_WARNING'; break;
466 case 1024: $e_type = 'E_USER_NOTICE'; break;
467 case 2048: $e_type = 'E_STRICT'; break;
468 case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break;
469 case 8192: $e_type = 'E_DEPRECATED'; break;
470 case 16384: $e_type = 'E_USER_DEPRECATED'; break;
471 case 30719: $e_type = 'E_ALL'; break;
472 default: $e_type = "E_UNKNOWN ($errno)"; break;
473 }
474
475 if (!is_string($errstr)) $errstr = serialize($errstr);
476
477 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
478
479 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
480
481 }
482
483 /**
484 * Runs upon the WordPress 'init' action
485 */
486 public function init() {
487 if ((!is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) && is_user_logged_in() && file_exists($this->includes_dir().'/tfa_frontend.php')) {
488 $this->load_frontend();
489 } else {
490 add_shortcode('twofactor_user_settings', array($this, 'shortcode_when_not_logged_in'));
491 }
492 }
493
494 /**
495 * Return the Simba_TFA_Provider_TOTP object.
496 *
497 * @returns Simba_TFA_Provider_TOTP
498 */
499 public function get_totp_controller() {
500 return $this->totp_controller;
501 }
502
503 /**
504 * "Shared" - i.e. could be called from either front-end or back-end
505 */
506 public function shared_ajax() {
507
508 if (empty($_POST['subaction']) || empty($_POST['nonce']) || !is_user_logged_in() || !wp_verify_nonce($_POST['nonce'], 'tfa_shared_nonce')) die('Security check (3).');
509
510 global $current_user;
511
512 $subaction = $_POST['subaction'];
513
514 if ('refreshotp' == $subaction) {
515
516 $code = $this->totp_controller->get_current_code($current_user->ID);
517
518 if (false === $code) die(json_encode(array('code' => '')));
519
520 die(json_encode(array('code' => $code)));
521
522 } elseif ('untrust_device' == $subaction && isset($_POST['device_id'])) {
523 $this->untrust_device(stripslashes($_POST['device_id']));
524 ob_start();
525 $this->include_template('trusted-devices-inner-box.php', array('trusted_devices' => $this->user_get_trusted_devices()));
526 echo json_encode(array('trusted_list' => ob_get_clean()));
527 }
528
529 exit;
530
531 }
532
533 /**
534 * Mark a device as untrusted for the current user
535 *
536 * @param String $device_id
537 */
538 protected function untrust_device($device_id) {
539
540 $trusted_devices = $this->user_get_trusted_devices();
541
542 unset($trusted_devices[$device_id]);
543
544 global $current_user;
545 $current_user_id = $current_user->ID;
546
547 $this->user_set_trusted_devices($current_user_id, $trusted_devices);
548
549 }
550
551 /**
552 * Called upon the AJAX action simbatfa-init-otp . Will die.
553 *
554 * Uses these keys from $_POST: user
555 */
556 public function tfaInitLogin() {
557
558 if (empty($_POST['user'])) die('Security check (2).');
559
560 if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) {
561 $res = array('result' => false, 'user_can_trust' => false);
562 } else {
563
564 if (!function_exists('sanitize_user')) require_once ABSPATH.WPINC.'/formatting.php';
565
566 // WP's password-checking sanitizes the supplied user, so we must do the same to check if TFA is enabled for them
567 $auth_info = array('log' => sanitize_user(stripslashes((string)$_POST['user'])));
568
569 if (!empty($_COOKIE['simbatfa_trust_token'])) $auth_info['trust_token'] = (string) $_COOKIE['simbatfa_trust_token'];
570
571 $res = $this->pre_auth($auth_info, 'array');
572 }
573
574 $results = array(
575 'jsonstarter' => 'justhere',
576 'status' => $res['result'],
577 );
578
579 if (!empty($res['user_can_trust'])) {
580 $results['user_can_trust'] = 1;
581 if (!empty($res['user_already_trusted'])) $results['user_already_trusted'] = 1;
582 }
583
584
585 if (!empty($this->output_buffering)) {
586 if (!empty($this->logged)) {
587 $results['php_output'] = $this->logged;
588 }
589 restore_error_handler();
590 $buffered = ob_get_clean();
591 if ($buffered) $results['extra_output'] = $buffered;
592 }
593
594 $results = apply_filters('simbatfa_check_tfa_requirements_ajax_response', $results);
595
596 echo json_encode($results);
597
598 exit;
599 }
600
601 /**
602 * Enable or disable TFA for a user
603 *
604 * @param Integer $user_id - the WordPress user ID
605 * @param String $setting - either "true" (to turn on) or "false" (to turn off)
606 */
607 public function change_tfa_enabled_status($user_id, $setting) {
608 $previously_enabled = $this->is_activated_by_user($user_id) ? 1 : 0;
609 $setting = ('true' === $setting) ? 1 : 0;
610 update_user_meta($user_id, 'tfa_enable_tfa', $setting);
611 do_action('simba_tfa_activation_status_saved', $user_id, $setting, $previously_enabled, $this);
612 }
613
614 /**
615 * Here's where the login action happens. Called on the WP 'authenticate' action.
616 *
617 * @param WP_Error|WP_User $user
618 * @param String $username - this is not necessarily the WP username; it is whatever was typed in the form, so can be an email address
619 * @param String $password
620 *
621 * @return WP_Error|WP_User
622 */
623 public function tfaVerifyCodeAndUser($user, $username, $password) {
624
625 $original_user = $user;
626 $params = stripslashes_deep($_POST);
627
628 // If (only) the error was a wrong password, but it looks like the user appended a TFA code to their password, then have another go
629 if (is_wp_error($user) && array('incorrect_password') == $user->get_error_codes() && !isset($params['two_factor_code']) && false !== ($from_password = apply_filters('simba_tfa_tfa_from_password', false, $password))) {
630 // This forces a new password authentication below
631 $user = false;
632 }
633
634 if (is_wp_error($user)) {
635 $ret = $user;
636 } else {
637
638 if (is_object($user) && isset($user->ID) && isset($user->user_login)) {
639 $params['log'] = $user->user_login;
640 // Confirm that this is definitely a username regardless of its format
641 $may_be_email = false;
642 } else {
643 $params['log'] = $username;
644 $may_be_email = true;
645 }
646
647 $params['caller'] = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['REQUEST_URI'];
648 if (!empty($_COOKIE['simbatfa_trust_token'])) $params['trust_token'] = (string) $_COOKIE['simbatfa_trust_token'];
649
650 if (isset($from_password) && false !== $from_password) {
651 // Support login forms that can't be hooked via appending to the password
652 $speculatively_try_appendage = true;
653 $params['two_factor_code'] = $from_password['tfa_code'];
654 }
655
656 $code_ok = $this->authorise_user_from_login($params, $may_be_email);
657
658 if (is_wp_error($code_ok)) {
659 $ret = $code_ok;
660 } elseif (!$code_ok) {
661 $ret = new WP_Error('authentication_failed', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('The one-time password (TFA code) you entered was incorrect.', 'two-factor-authentication'));
662 } elseif ($user) {
663 $ret = $user;
664 } else {
665
666 if (!empty($speculatively_try_appendage) && true === $code_ok) {
667 $password = $from_password['password'];
668 }
669
670 $username_is_email = false;
671
672 if (function_exists('wp_authenticate_username_password') && $may_be_email && filter_var($username, FILTER_VALIDATE_EMAIL)) {
673 global $wpdb;
674 // This has to match self::authorise_user_from_login()
675 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $username));
676 if (is_object($response)) $username_is_email = true;
677 }
678
679 $ret = $username_is_email ? wp_authenticate_email_password(null, $username, $password) : wp_authenticate_username_password(null, $username, $password);
680 }
681
682 }
683
684 $ret = apply_filters('simbatfa_verify_code_and_user_result', $ret, $original_user, $username, $password);
685
686 // If the TFA code was actually validated (not just not required, for example), then $code_ok is (boolean)true
687 if (isset($code_ok) && true === $code_ok && is_a($ret, 'WP_User')) {
688 if (!empty($params['simba_tfa_mark_as_trusted']) && $this->user_can_trust($ret->ID) && (is_ssl() || (!empty($_SERVER['SERVER_NAME']) && ('localhost' == $_SERVER['SERVER_NAME'] ||'127.0.0.1' == $_SERVER['SERVER_NAME'])))) {
689
690 $trusted_for = $this->get_option('tfa_trusted_for');
691 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
692
693 $this->trust_device($ret->ID, $trusted_for);
694 }
695 }
696
697 return $ret;
698 }
699
700 // N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
701 public function user_can_trust($user_id) {
702 // Default is false because this is a new feature and we don't want to surprise existing users by granting broader access than they expected upon an upgrade
703 return apply_filters('simba_tfa_user_can_trust', false, $user_id);
704 }
705
706 /**
707 * Should the user be asked for a TFA code? And optionally, is the user allowed to trust devices?
708 *
709 * @param Array $params - the key used is 'log', indicating the username or email address
710 * @param String $response_format - 'simple' (historic format) or 'array' (richer info)
711 *
712 * @return Boolean
713 */
714 public function pre_auth($params, $response_format = 'simple') {
715 global $wpdb;
716
717 $query = filter_var($params['log'], FILTER_VALIDATE_EMAIL) ? $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']);
718 $user = $wpdb->get_row($query);
719
720 if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
721 // Corner-case: login looks like an email, but is a username rather than email address
722 $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']));
723 }
724
725 $is_activated_for_user = true;
726 $is_activated_by_user = false;
727
728 $result = false;
729
730 $totp_controller = $this->totp_controller;
731
732 if ($user) {
733 $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true);
734 $is_activated_for_user = $this->is_activated_for_user($user->ID);
735 $is_activated_by_user = $this->is_activated_by_user($user->ID);
736
737 if ($is_activated_for_user && $is_activated_by_user) {
738
739 // No private key yet, generate one. This shouldn't really be possible.
740 if (!$tfa_priv_key) $tfa_priv_key = $totp_controller->addPrivateKey($user->ID);
741
742 $code = $totp_controller->generateOTP($user->ID, $tfa_priv_key);
743
744 $result = true;
745 }
746 }
747
748 if ('array' != $response_format) return $result;
749
750 $ret = array('result' => $result);
751
752 if ($result) {
753 $ret['user_can_trust'] = $this->user_can_trust($user->ID);
754 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user->ID, $params['trust_token'])) {
755 $ret['user_already_trusted'] = 1;
756 }
757 }
758
759 return $ret;
760 }
761
762 /**
763 * Print the radio buttons for enabling/disabling TFA
764 *
765 * @param Integer $user_id - the WordPress user ID
766 * @param Boolean $long_label - whether to use a long label rather than a short one
767 * @param String $style - valid values are "show_current" and "require_current"
768 */
769 public function paint_enable_tfa_radios($user_id, $long_label = false, $style = 'show_current') {
770
771 if (!$user_id) return;
772
773 if ('require_current' != $style) $style = 'show_current';
774
775 $is_required = $this->is_required_for_user($user_id);
776 $is_activated = $this->is_activated_by_user($user_id);
777
778 if ($is_required) {
779 $require_after = absint($this->get_option('tfa_requireafter'));
780 echo '<p class="tfa_required_warning" style="font-weight:bold; font-style:italic;">'.sprintf(__('N.B. This site is configured to forbid you to log in if you disable two-factor authentication after your account is %d days old', 'two-factor-authentication'), $require_after).'</p>';
781 }
782
783 $tfa_enabled_label = $long_label ? __('Enable two-factor authentication', 'two-factor-authentication') : __('Enabled', 'two-factor-authentication');
784
785 if ('show_current' == $style) {
786 $tfa_enabled_label .= ' '.sprintf(__('(Current code: %s)', 'two-factor-authentication'), $this->get_totp_controller()->current_otp_code($user_id));
787 } elseif ('require_current' == $style) {
788 $tfa_enabled_label .= ' '.sprintf(__('(you must enter the current code: %s)', 'two-factor-authentication'), '<input type="text" class="tfa_enable_current" name="tfa_enable_current" size="6" style="height">');
789 }
790
791 $show_disable = ((is_multisite() && is_super_admin()) || (!is_multisite() && current_user_can($this->get_management_capability())) || false == $is_activated || !$is_required || !$this->get_option('tfa_hide_turn_off')) ? true : false;
792
793 $tfa_disabled_label = $long_label ? __('Disable two-factor authentication', 'two-factor-authentication') : __('Disabled', 'two-factor-authentication');
794
795 if ('require_current' == $style) echo '<input type="hidden" name="require_current" value="1">'."\n";
796
797 echo '<input type="radio" class="tfa_enable_radio" id="tfa_enable_tfa_true" name="tfa_enable_tfa" value="true" '.(true == $is_activated ? 'checked="checked"' : '').'> <label class="tfa_enable_radio_label" for="tfa_enable_tfa_true">'.apply_filters('simbatfa_radiolabel_enabled', $tfa_enabled_label, $long_label).'</label> <br>';
798
799 // Show the 'disabled' option if the user is an admin, or if it is currently set, or if TFA is not compulsory, or if the site owner doesn't require it to be hidden
800 // Note that this just hides the option in the UI. The user could POST to turn off TFA, but, since it's required, they won't be able to log in.
801 if ($show_disable) {
802 echo '<input type="radio" class="tfa_enable_radio" id="tfa_enable_tfa_false" name="tfa_enable_tfa" value="false" '.(false == $is_activated ? 'checked="checked"' :'').'> <label class="tfa_enable_radio_label" for="tfa_enable_tfa_false">'.apply_filters('simbatfa_radiolabel_disabled', $tfa_disabled_label, $long_label).'</label> <br>';
803 }
804 }
805
806 /**
807 * Retrieve a saved option
808 *
809 * @param String $key - option key
810 *
811 * @return Mixed
812 */
813 public function get_option($key) {
814 if (!is_multisite()) return get_option($key);
815 switch_to_blog(1);
816 $v = get_option($key);
817 restore_current_blog();
818 return $v;
819 }
820
821 /**
822 * Paint a list of checkboxes, one for each role
823 *
824 * @param String $prefix
825 * @param Integer $default - default value (0 or 1)
826 */
827 public function list_user_roles_checkboxes($prefix = '', $default = 1) {
828
829 if (is_multisite()) {
830 // Not a real WP role; needs separate handling
831 $id = '_super_admin';
832 $name = __('Multisite Super Admin', 'two-factor-authentication');
833 $setting = $this->get_option('tfa_'.$prefix.$id);
834 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
835
836 echo '<input type="checkbox" id="tfa_'.$prefix.$id.'" name="tfa_'.$prefix.$id.'" value="1" '.($setting ? 'checked="checked"' :'').'> <label for="tfa_'.$prefix.$id.'">'.htmlspecialchars($name)."</label><br>\n";
837 }
838
839 global $wp_roles;
840 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
841
842 foreach ($wp_roles->role_names as $id => $name) {
843 $setting = $this->get_option('tfa_'.$prefix.$id);
844 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
845
846 echo '<input type="checkbox" id="tfa_'.$prefix.$id.'" name="tfa_'.$prefix.$id.'" value="1" '.($setting ? 'checked="checked"' :'').'> <label for="tfa_'.$prefix.$id.'">'.htmlspecialchars($name)."</label><br>\n";
847 }
848
849 }
850
851 public function tfa_list_xmlrpc_status_radios() {
852
853 $setting = $this->get_option('tfa_xmlrpc_on');
854 $setting = $setting ? 1 : 0;
855
856 $types = array(
857 '0' => __('Do not require 2FA over XMLRPC (best option if you must use XMLRPC and your client does not support 2FA)', 'two-factor-authentication'),
858 '1' => __('Do require 2FA over XMLRPC (best option if you do not use XMLRPC or are unsure)', 'two-factor-authentication')
859 );
860
861 foreach($types as $id => $name) {
862 print '<input type="radio" name="tfa_xmlrpc_on" id="tfa_xmlrpc_on_'.$id.'" value="'.$id.'" '.($setting == $id ? 'checked="checked"' : '').'> <label for="tfa_xmlrpc_on_'.$id.'">'.htmlspecialchars($name)."</label><br>\n";
863 }
864 }
865
866 protected function is_caller_active() {
867
868 if (!defined('XMLRPC_REQUEST') || !XMLRPC_REQUEST) return true;
869
870 $saved_data = $this->get_option('tfa_xmlrpc_on');
871
872 return $saved_data ? true : false;
873
874 }
875
876 /**
877 * @param Array $params
878 * @param Boolean $may_be_email
879 *
880 * @return WP_Error|Boolean|Integer - WP_Error or false means failure; true or 1 means success, but true means the TFA code was validated
881 */
882 public function authorise_user_from_login($params, $may_be_email = false) {
883
884 $params = apply_filters('simbatfa_auth_user_from_login_params', $params);
885
886 global $wpdb;
887
888 if (!$this->is_caller_active()) return 1;
889
890 $query = ($may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) ? $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $params['log']) : $wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']);
891 $response = $wpdb->get_row($query);
892
893 if (!$response && $may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
894 // Corner-case: login looks like an email, but is a username rather than email address
895 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']));
896 }
897
898 $user_ID = is_object($response) ? $response->ID : false;
899 $user_registered = is_object($response) ? $response->user_registered : false;
900
901 $user_code = isset($params['two_factor_code']) ? str_replace(' ', '', trim($params['two_factor_code'])) : '';
902
903 // This condition in theory should not be possible
904 if (!$user_ID) return new WP_Error('tfa_user_not_found', apply_filters('simbatfa_tfa_user_not_found', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('The indicated user could not be found.', 'two-factor-authentication')));
905
906 if (!$this->is_activated_for_user($user_ID)) return 1;
907
908 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user_ID, $params['trust_token'])) {
909 return 1;
910 }
911
912 if (!$this->is_activated_by_user($user_ID)) {
913
914 if (!$this->is_required_for_user($user_ID)) return 1;
915
916 $require_after = absint($this->get_option('tfa_requireafter')) * 86400;
917
918 $account_age = time() - strtotime($user_registered);
919
920 if ($account_age > $require_after && apply_filters('simbatfa_enforce_require_after_check', true, $user_ID, $require_after, $account_age)) {
921 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('The site owner has forbidden you to login without two-factor authentication. Please contact the site owner to re-gain access.', 'two-factor-authentication')));
922 }
923
924 return 1;
925 }
926
927 $tfa_creds_user_id = !empty($params['creds_user_id']) ? $params['creds_user_id'] : $user_ID;
928
929 if ($tfa_creds_user_id != $user_ID) {
930
931 // Authenticating using a different user's credentials (e.g. https://wordpress.org/plugins/use-administrator-password/)
932 // In this case, we require that different user to have TFA active - so that this mechanism can't be used to avoid TFA
933
934 if (!$this->is_activated_for_user($tfa_creds_user_id) || !$this->is_activated_by_user($tfa_creds_user_id)) {
935 return new WP_Error('tfa_required', apply_filters('simbatfa_notfa_forbidden_login_altuser', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.__('You are attempting to log in to an account that has two-factor authentication enabled; this requires you to also have two-factor authentication enabled on the account whose credentials you are using.', 'two-factor-authentication')));
936 }
937
938 }
939
940 return $this->totp_controller->check_code_for_user($tfa_creds_user_id, $user_code);
941
942 }
943
944 /**
945 * Evaluate whether a trust token is valid for a user
946 *
947 * @param Integer $user_id - WP user ID
948 * @param String $trust_token - trust token
949 *
950 * @return Boolean
951 */
952 protected function user_trust_token_valid($user_id, $trust_token) {
953
954 if (!is_string($trust_token) || strlen($trust_token) < 30) return false;
955
956 $trusted_devices = $this->user_get_trusted_devices($user_id);
957
958 $time_now = time();
959
960 foreach ($trusted_devices as $device) {
961 if (empty($device['until']) || $device['until'] <= $time_now) continue;
962 if (!empty($device['token']) && $device['token'] === $trust_token) {
963 return true;
964 }
965 }
966
967 return false;
968 }
969
970 /**
971 * This deals with the issue that wp-login.php does not redirect to a canonical URL. As a result, if a website is available under more than one host, then admin_url('admin-ajax.php') might return a different one than the visitor is using, resulting in AJAX failing due to CORS errors.
972 *
973 * @return String
974 */
975 protected function get_ajax_url() {
976 $ajax_url = admin_url('admin-ajax.php');
977 $parsed_url = parse_url($ajax_url);
978 if (strtolower($parsed_url['host']) !== strtolower($_SERVER['HTTP_HOST']) && !empty($parsed_url['path'])) {
979 // Mismatch - return the relative URL only
980 $ajax_url = $parsed_url['path'];
981 }
982 return $ajax_url;
983 }
984
985 /**
986 * Called not only upon the WP action login_enqueue_scripts, but potentially upon the action 'init' and various others from other plugins too. It can handle being called multiple times.
987 */
988 public function login_enqueue_scripts() {
989
990 if (isset($_GET['action']) && 'logout ' != $_GET['action'] && 'login' != $_GET['action']) return;
991
992 static $already_done = false;
993 if ($already_done) return;
994 $already_done = true;
995
996 // Prevent cacheing when in debug mode
997 $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/tfa.js');
998
999 wp_enqueue_script('tfa-ajax-request', $this->includes_url().'/tfa.js', array('jquery'), $script_ver);
1000
1001 $trusted_for = $this->get_option('tfa_trusted_for');
1002 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1003
1004 $localize = array(
1005 'ajaxurl' => $this->get_ajax_url(),
1006 'click_to_enter_otp' => __("Click to enter One Time Password", 'two-factor-authentication'),
1007 'enter_username_first' => __('You have to enter a username first.', 'two-factor-authentication'),
1008 'otp' => __('One Time Password (i.e. 2FA)', 'two-factor-authentication'),
1009 'otp_login_help' => __('(check your OTP app to get this password)', 'two-factor-authentication'),
1010 'mark_as_trusted' => sprintf(_n('Trust this device (allow login without 2FA for %d day)', 'Trust this device (allow login without TFA for %d days)', $trusted_for, 'two-factor-authentication'), $trusted_for),
1011 'is_trusted' => __('(Trusted device)', 'two-factor-authentication'),
1012 'nonce' => wp_create_nonce('simba_tfa_loginform_nonce'),
1013 'login_form_selectors' => '',
1014 'login_form_off_selectors' => '',
1015 );
1016
1017 // Spinner exists since WC 3.8. Use the proper functions to avoid SSL warnings.
1018 if (file_exists(ABSPATH.'wp-admin/images/spinner-2x.gif')) {
1019 $localize['spinnerimg'] = admin_url('images/spinner-2x.gif');
1020 } elseif (file_exists(ABSPATH.WPINC.'/images/spinner-2x.gif')) {
1021 $localize['spinnerimg'] = includes_url('images/spinner-2x.gif');
1022 }
1023
1024 $localize = apply_filters('simba_tfa_login_enqueue_localize', $localize);
1025
1026 wp_localize_script('tfa-ajax-request', 'simba_tfasettings', $localize);
1027
1028 }
1029
1030 /**
1031 * Return or output view content
1032 *
1033 * @param String $path - path to template, usually relative to templates/ within the plugin directory
1034 * @param Array $extract_these - key/value pairs for substitution into the scope of the template
1035 * @param Boolean $return_instead_of_echo - what to do with the results
1036 *
1037 * @return String|Void
1038 */
1039 public function include_template($path, $extract_these = array(), $return_instead_of_echo = false) {
1040
1041 if ($return_instead_of_echo) ob_start();
1042
1043 $template_file = apply_filters('simatfa_template_file', $this->templates_dir().'/'.$path, $path, $extract_these, $return_instead_of_echo);
1044
1045 do_action('simbatfa_before_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1046
1047 if (!file_exists($template_file)) {
1048 error_log("TFA: template not found: $template_file (from $path)");
1049 echo __('Error:', 'two-factor-authentication').' '.__('two-factor-authentication', 'wp-optimize')." (".$path.")";
1050 } else {
1051 extract($extract_these);
1052 // The following are useful variables which can be used in the template.
1053 // They appear as unused, but may be used in the $template_file.
1054 $wpdb = $GLOBALS['wpdb'];// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wpdb might be used in the included template
1055 $simba_tfa = $this;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1056 include $template_file;
1057 }
1058
1059 do_action('simbatfa_after_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1060
1061 if ($return_instead_of_echo) return ob_get_clean();
1062 }
1063
1064 /**
1065 * Make sure that self::$frontend is the instance of Simba_TFA_Frontend, and return it
1066 *
1067 * @return Simba_TFA_Frontend
1068 */
1069 public function load_frontend() {
1070 if (!class_exists('Simba_TFA_Frontend')) require_once($this->includes_dir().'/tfa_frontend.php');
1071 if (empty($this->frontend)) $this->frontend = new Simba_TFA_Frontend($this);
1072 return $this->frontend;
1073 }
1074
1075 // __return_empty_string() does not exist until WP 3.7
1076 public function shortcode_when_not_logged_in() {
1077 return '';
1078 }
1079
1080 }
1081