PluginProbe
Two Factor Authentication / 1.14.17
Two Factor Authentication v1.14.17
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.17, at simba-tfa/simba-tfa.php

1,669 lines 57.7 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_1 {
6
7 /**
8 * Simba 2FA frontend object
9 *
10 * @var Object
11 */
12 protected $frontend;
13
14 /**
15 * Simba 2FA TOTP object
16 *
17 * @var Object
18 */
19 protected $controllers = array();
20
21 /**
22 * Flag for prevent PHP notices in AJAX
23 *
24 * @var Boolean
25 */
26 private $output_buffering;
27
28 /**
29 * Logged error lines array
30 *
31 * @var Array
32 */
33 private $logged;
34
35 /**
36 * URL slug for the plugin's option page
37 *
38 * @var String
39 */
40 private $user_settings_page_slug;
41
42 /**
43 * Settings page heading for plugin's option page
44 *
45 * @var String
46 */
47 private $settings_page_heading;
48
49 /**
50 * Plugin translate url
51 *
52 * @var string
53 */
54 private $plugin_translate_url;
55
56 /**
57 * URL slug for the site-wide administration options
58 *
59 * @var String
60 */
61 private $site_wide_administration_url;
62
63 /**
64 * URL for the premium version
65 *
66 * @var String
67 */
68 private $premium_version_url;
69
70 /**
71 * URL for the FAQ
72 *
73 * @var String
74 */
75 private $faq_url;
76
77 /**
78 * Authentication slug. Verify that two-factor authentication should not be repeated for the same slug.
79 *
80 * @var String
81 */
82 private $authentication_slug = 'updraft';
83
84 private static $is_authenticated = array();
85
86 private $tfa_muplugin;
87
88 /**
89 * Class Constructor, Set basic settings.
90 *
91 * @return Void
92 */
93 public function __construct() {
94
95 if (!defined('TFA_INCORRECT_MAX_ATTEMPTS_ALLOWED_LIMIT')) define('TFA_INCORRECT_MAX_ATTEMPTS_ALLOWED_LIMIT', 5);
96 if (!defined('TFA_INCORRECT_ATTEMPTS_WITHIN_MINUTES_LIMIT')) define('TFA_INCORRECT_ATTEMPTS_WITHIN_MINUTES_LIMIT', 30);
97
98 $load_providers = apply_filters('simbatfa_load_providers', array('totp'));
99
100 foreach ($load_providers as $provider_id) {
101 $class_name = "Simba_TFA_Provider_$provider_id";
102 if (!class_exists($class_name)) {
103 require_once(__DIR__.'/providers/'.$provider_id.'/loader.php');
104 }
105 $this->controllers[$provider_id] = new $class_name($this);
106 }
107
108 // Process login form AJAX events
109 add_action('wp_ajax_nopriv_simbatfa-init-otp', array($this, 'tfaInitLogin'));
110 add_action('wp_ajax_simbatfa-init-otp', array($this, 'tfaInitLogin'));
111
112 add_action('wp_ajax_simbatfa_shared_ajax', array($this, 'shared_ajax'));
113
114 if (!class_exists('Simba_TFA_Login_Form_Integrations')) require_once($this->includes_dir().'/login-form-integrations.php');
115 new Simba_TFA_Login_Form_Integrations($this);
116
117 if (!class_exists('Simba_TFA_Encryption_Muplugin')) require_once($this->includes_dir().'/tfa-encryption-muplugin.php');
118 $this->tfa_muplugin = new Simba_TFA_Encryption_Muplugin($this);
119
120 // Add TFA column on admin users list
121 add_action('manage_users_columns', array($this, 'manage_users_columns_tfa'));
122 add_action('wpmu_users_columns', array($this, 'manage_users_columns_tfa'));
123 add_action('manage_users_custom_column', array($this, 'manage_users_custom_column_tfa'), 10, 3);
124
125 // CSS for admin users screen
126 add_action('admin_print_styles-users.php', array($this, 'load_users_css'), 10, 0);
127
128 add_action('admin_menu', array($this, 'admin_menu'), 9);
129
130 add_action('admin_init', array($this, 'register_two_factor_auth_settings'));
131 add_action('init', array($this, 'init'));
132
133 if (!defined('TWO_FACTOR_DISABLE') || !TWO_FACTOR_DISABLE) {
134 add_filter('authenticate', array($this, 'tfaVerifyCodeAndUser'), 99999999999, 3);
135 }
136
137 add_action('show_user_profile', array($this, 'show_user_profile'), 1);
138
139 add_action('enqueue_block_assets', array($this, 'enqueue_gutenberg_block_scripts'));
140
141 add_filter('pre_update_option', array($this, 'setup_secret_encryption'), 10, 2);
142
143 if (defined('DOING_AJAX') && DOING_AJAX && defined('WP_ADMIN') && WP_ADMIN && !empty($_REQUEST['action']) && 'simbatfa-init-otp' == $_REQUEST['action']) {
144 // Try to prevent PHP notices breaking the AJAX conversation
145 $this->output_buffering = true;
146 $this->logged = array();
147 set_error_handler(array($this, 'get_php_errors'), E_ALL & ~E_STRICT);
148 ob_start();
149 }
150 }
151
152 /**
153 * Runs upon the WP action show_user_profile
154 *
155 * @param WP_User $user - the user that the profile is for
156 */
157 public function show_user_profile($user) {
158 if ($user->ID !== get_current_user_id() || !$this->is_activated_for_user($user->ID)) return;
159 echo '<h2>'.__('Two Factor Authentication', 'two-factor-authentication').'</h2>';
160 $settings_url = admin_url('admin.php').'?page='.$this->get_user_settings_page_slug();
161 printf('<a target="_blank" href="%s">%s</a>', $settings_url, __('Go here for your two factor authentication settings...', 'two-factor-authentication'));
162 }
163
164 /**
165 * Enqueues scripts for Gutenberg blocks.
166 *
167 * @return void
168 */
169 public function enqueue_gutenberg_block_scripts() {
170 global $pagenow;
171
172 if ($pagenow == 'post.php' || has_block('twofactor/user-settings')) {
173 $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir() . '/gutenberg-blocks.js');
174 wp_enqueue_script('twofactor-gutenberg-blocks', $this->includes_url() . '/gutenberg-blocks.js', array('wp-blocks', 'wp-element', 'wp-server-side-render'), $script_ver);
175
176 wp_localize_script('twofactor-gutenberg-blocks', 'tfa_trans',
177 array(
178 'block_title' => __('Two Factor Authentication Settings', 'two-factor-authentication'),
179 )
180 );
181 }
182 }
183
184 /**
185 * This function is called via the filter `pre_update_option` if the option being saved is `tfa_encrypt_secrets` then we will proceed to setup the encryption
186 *
187 * @param mixed $value - the value of the option
188 * @param string $option_name - the option name
189 *
190 * @return mixed - returns 0 on error to prevent the feature from being turned on otherwise returns the value passed in
191 */
192 public function setup_secret_encryption($value, $option_name) {
193 if ('tfa_encrypt_secrets' !== $option_name) return $value;
194
195 if (!$this->tfa_muplugin->muplugin_exists() || !defined('SIMBA_TFA_DB_ENCRYPTION_KEY') || '' === SIMBA_TFA_DB_ENCRYPTION_KEY) {
196 $result = $this->tfa_muplugin->insert_contents();
197
198 if (is_wp_error($result)) {
199 add_settings_error('tfa_encrypt_secrets', $result->get_error_code(), $result->get_error_message());
200 return 0;
201 }
202
203 // We now need to include the file as it won't be loaded until WordPress refreshes but we want to use it now
204 include_once($this->tfa_muplugin->get_file_path());
205 }
206
207 $result = $this->get_controller('totp')->potentially_encrypt_private_keys();
208
209 if (is_wp_error($result)) {
210 add_settings_error('tfa_encrypt_secrets', $result->get_error_code(), $result->get_error_message());
211 return 0;
212 }
213
214 return $value;
215 }
216
217 /**
218 * Runs upon the WP filter admin_menu
219 */
220 public function admin_menu() {
221 $this->get_controller('totp')->potentially_port_private_keys();
222 }
223
224 /**
225 * Give the filesystem path to the plugin's includes directory
226 *
227 * @return String
228 */
229 public function includes_dir() {
230 return __DIR__.'/includes';
231 }
232
233 /**
234 * Give the URL for the plugin's includes directory
235 *
236 * @return String
237 */
238 public function includes_url() {
239 return plugins_url('', __FILE__).'/includes';
240 }
241
242 /**
243 * Set URL slug for the plugin's option page.
244 *
245 * @param String Setting page URL slug.
246 * @return Void
247 */
248 public function set_user_settings_page_slug($user_settings_page_slug) {
249 $this->user_settings_page_slug = $user_settings_page_slug;
250 }
251
252 /**
253 * Get URL slug for the plugin's option page.
254 *
255 * @return String Setting page URL slug.
256 */
257 public function get_user_settings_page_slug() {
258 return $this->user_settings_page_slug;
259 }
260
261 /**
262 * Set settings page heading for plugin's option page
263 *
264 * @param String $settings_page_heading String.
265 *
266 * @return String
267 */
268 public function set_settings_page_heading($settings_page_heading) {
269 $this->settings_page_heading = $settings_page_heading;
270 }
271
272 /**
273 * Get settings page heading for plugin's option page.
274 *
275 * @return String Setting page heading.
276 */
277 public function get_settings_page_heading() {
278 return $this->settings_page_heading;
279 }
280
281 /**
282 * Set plugin translate url
283 *
284 * @param String $plugin_translate_url Plugin translation URL.
285 * @return Void
286 */
287 public function set_plugin_translate_url($plugin_translate_url) {
288 $this->plugin_translate_url = $plugin_translate_url;
289 }
290
291 /**
292 * Get plugin translate url
293 *
294 * @return String Plugin translate URL
295 */
296 public function get_plugin_translate_url() {
297 return $this->plugin_translate_url;
298 }
299
300 /**
301 * Set plugin premium version url
302 *
303 * @param String $premium_version_url Plugin premium version url.
304 * @return Void
305 */
306 public function set_premium_version_url($premium_version_url) {
307 $this->premium_version_url = $premium_version_url;
308 }
309
310 /**
311 * Get plugin premium version URL.
312 *
313 * @return String Plugin premium version URL.
314 */
315 public function get_premium_version_url() {
316 return $this->premium_version_url;
317 }
318
319 /**
320 * Set plugin FAQ URL
321 *
322 * @param String $faq_url Plugin FAQ URL.
323 * @return Void
324 */
325 public function set_faq_url($faq_url) {
326 $this->faq_url = $faq_url;
327 }
328
329 /**
330 * Get plugin FAQ URL.
331 *
332 * @return String Plugin FAQ URL.
333 */
334 public function get_faq_url() {
335 return $this->faq_url;
336 }
337
338 /**
339 * Set plugin site wide administration URL
340 *
341 * @param String $site_wide_administration_url Plugin site wide administration URL.
342 * @return Void
343 */
344 public function set_site_wide_administration_url($site_wide_administration_url) {
345 $this->site_wide_administration_url = $site_wide_administration_url;
346 }
347
348 /**
349 * Get plugin site wide administration URL.
350 *
351 * @return String Plugin site wide administration URL
352 */
353 public function get_site_wide_administration_url() {
354 return $this->site_wide_administration_url;
355 }
356
357 /**
358 * Give the filesystem path to the plugin's templates directory
359 *
360 * @return String
361 */
362 public function templates_dir() {
363 return __DIR__.'/templates';
364 }
365
366 /**
367 * Include the user settings page code
368 */
369 public function show_dashboard_user_settings_page() {
370 $this->include_template('user-settings.php');
371 }
372
373 /**
374 * Enqueue CSS styling on the users page
375 */
376 public function load_users_css() {
377 $css_version = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/users.css');
378 wp_enqueue_style(
379 'tfa-users-css',
380 $this->includes_url().'/users.css',
381 array(),
382 $css_version,
383 'screen'
384 );
385 }
386
387 /**
388 * Add the 2FA label to the users list table header.
389 *
390 * @param Array $columns Table columns.
391 *
392 * @return Array
393 */
394 public function manage_users_columns_tfa($columns = array()) {
395 $columns['tfa-status'] = __('2FA', 'two-factor-authentication');
396 return $columns;
397 }
398
399 /**
400 * Add status into TFA column.
401 *
402 * @param String $value String.
403 * @param String $column_name Column name.
404 * @param Integer $user_id User ID.
405 *
406 * @return String
407 */
408 public function manage_users_custom_column_tfa($value = '', $column_name = '', $user_id = 0) {
409
410 // Only for this column name.
411 if ('tfa-status' === $column_name) {
412
413 if (!$this->is_activated_for_user($user_id)) {
414 $value = '&#8212;';
415 } elseif ($this->is_activated_by_user($user_id)) {
416 // Use value.
417 $value = '<span title="'.__('Enabled', 'two-factor-authentication').'" class="dashicons dashicons-yes"></span>';
418 } else {
419 // No group.
420 $value = '<span title="'.__('Disabled', 'two-factor-authentication').'" class="dashicons dashicons-no"></span>';
421 }
422 }
423
424 return $value;
425 }
426
427 /**
428 * Paint out an admin notice
429 *
430 * @param String $message - the caller should already have taken care of any escaping
431 * @param String $class
432 */
433 public function show_admin_warning($message, $class = 'updated') {
434 echo '<div class="tfamessage '.$class.'">'."<p>$message</p></div>";
435 }
436
437 /**
438 * Returns all two factor authentication setting name => group pairs.
439 *
440 * @return Array
441 */
442 private function get_config_keys() {
443 global $wp_roles;
444
445 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
446
447 $keys = array(
448 'tfa_requireafter' => 'tfa_user_roles_required_group',
449 'tfa_require_enforce_after' => 'tfa_user_roles_required_group',
450 'tfa_if_required_redirect_to' => 'tfa_user_roles_required_group',
451 'tfa_hide_turn_off' => 'tfa_user_roles_required_group',
452 'tfa_trusted_for' => 'tfa_user_roles_trusted_group',
453 'tfa_wc_add_section' => 'simba_tfa_woocommerce_group',
454 'tfa_bot_protection' => 'simba_tfa_woocommerce_group',
455 'tfa_default_hmac' => 'simba_tfa_default_hmac_group',
456 'tfa_encrypt_secrets' => 'simba_tfa_encrypt_secrets_group',
457 'tfa_xmlrpc_on' => 'tfa_xmlrpc_status_group',
458 );
459
460 foreach ($wp_roles->role_names as $id => $name) {
461 $keys['tfa_'.$id] = 'tfa_user_roles_group';
462 $keys['tfa_trusted_'.$id] = 'tfa_user_roles_trusted_group';
463 $keys['tfa_required_'.$id] = 'tfa_user_roles_required_group';
464 }
465
466 if (is_multisite()) {
467 $keys['tfa__super_admin'] = 'tfa_user_roles_group';
468 $keys['tfa_trusted__super_admin'] = 'tfa_user_roles_trusted_group';
469 $keys['tfa_required__super_admin'] = 'tfa_user_roles_required_group';
470 }
471
472 return $keys;
473 }
474
475 /**
476 * Registers all two factor authentication settings. Runs upon the WP action admin_init.
477 */
478 public function register_two_factor_auth_settings() {
479 $config_keys = $this->get_config_keys();
480
481 foreach ($config_keys as $name => $group) {
482 register_setting($group, $name);
483 }
484 }
485
486 /**
487 * Returns all two factor authentication options from the WP database.
488 *
489 * @return Array
490 */
491 public function get_configs() {
492 $config_keys = $this->get_config_keys();
493
494 $configs = array();
495
496 foreach (array_keys($config_keys) as $name) {
497 if (false !== $this->get_option($name)) {
498 $configs[$name] = $this->get_option($name);
499 }
500 }
501
502 return $configs;
503 }
504
505 /**
506 * Sets two factor authentication options from array.
507 *
508 * @param Array $configs
509 *
510 * @return Boolean
511 */
512 public function set_configs($configs) {
513 $result = false;
514
515 foreach ($configs as $key => $value) {
516 $result = $this->update_option($key, $value) ? true : $result;
517 }
518
519 return $result;
520 }
521
522 /**
523 * Deletes all two factor authentication options from the WP database.
524 *
525 * @return Void
526 */
527 public function delete_configs() {
528 $config_keys = $this->get_config_keys();
529
530 foreach (array_keys($config_keys) as $name) {
531 $this->delete_option($name);
532 }
533 }
534
535 /**
536 * See whether TFA is available or not for a particular user - i.e. whether the administrator has permitted it for their user level
537 *
538 * @param Integer $user_id - WordPress user ID
539 *
540 * @return Boolean
541 */
542 public function is_activated_for_user($user_id) {
543
544 if (empty($user_id)) return false;
545
546 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
547 if (is_multisite() && is_super_admin($user_id)) {
548 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
549 $role = '_super_admin';
550 $db_val = $this->get_option('tfa_'.$role);
551 // Defaults to true if no setting has been saved
552 return (false === $db_val || $db_val) ? true : false;
553 }
554
555 $roles = $this->get_user_roles($user_id);
556
557 // N.B. This populates with roles on the current site within a multisite
558 foreach ($roles as $role) {
559 $db_val = $this->get_option('tfa_'.$role);
560 if (false === $db_val || $db_val) return true;
561 }
562
563 return false;
564
565 }
566
567 /**
568 * Get all user roles for a given user (if on multisite, amalgamates all roles from all sites)
569 *
570 * @param Integer $user_id - WordPress user ID
571 *
572 * @return Array
573 */
574 protected function get_user_roles($user_id) {
575
576 // Get roles on the main site
577 $user = new WP_User($user_id);
578 $roles = (array) $user->roles;
579
580 // On multisite, also check roles on non-main sites
581 if (is_multisite()) {
582 global $wpdb, $table_prefix;
583 $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));
584 if (is_array($roles_db)) {
585 foreach ($roles_db as $role_info) {
586 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;
587 $site_roles = unserialize($role_info->meta_value);
588 if (!is_array($site_roles)) continue;
589 foreach ($site_roles as $role => $active) {
590 if ($active && !in_array($role, $roles)) $roles[] = $role;
591 }
592 }
593 }
594 }
595
596 return $roles;
597 }
598
599 /**
600 * Check if TFA is required for a specified user
601 *
602 * N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
603 *
604 * @param $user_id Integer - the WP user ID
605 *
606 * @return Boolean
607 */
608 public function is_required_for_user($user_id) {
609 return apply_filters('simba_tfa_required_for_user', $this->user_property_active($user_id, 'required_'), $user_id);
610 }
611
612 /**
613 * See if a particular user property is active
614 *
615 * @param Integer $user_id
616 * @param String $prefix - e.g. "required_", "trusted_"
617 *
618 * @return Boolean
619 */
620 public function user_property_active($user_id, $prefix = 'required_') {
621
622 if (empty($user_id)) return false;
623
624 // Super admin is not a role (they are admins with an extra attribute); needs separate handling
625 if (is_multisite() && is_super_admin($user_id)) {
626 // This is always a final decision - we don't want it to drop through to the 'admin' role's setting
627 $role = '_super_admin';
628 $db_val = $this->get_option('tfa_'.$prefix.$role);
629 return $db_val ? true : false;
630 }
631
632 $roles = $this->get_user_roles($user_id);
633
634 foreach ($roles as $role) {
635 $db_val = $this->get_option('tfa_'.$prefix.$role);
636 if ($db_val) return true;
637 }
638
639 return false;
640
641 }
642
643 /**
644 * 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.
645 *
646 * @param Integer $user_id
647 *
648 * @return Boolean
649 */
650 public function is_activated_by_user($user_id) {
651 $enabled = get_user_meta($user_id, 'tfa_enable_tfa', true);
652 return !empty($enabled);
653 }
654
655 /**
656 * Get a list of trusted devices for the user
657 *
658 * @param Integer|Boolean $user_id - WordPress user ID, or false for the current user
659 *
660 * @return Array
661 */
662 public function user_get_trusted_devices($user_id = false) {
663
664 if (false === $user_id) {
665 global $current_user;
666 $user_id = $current_user->ID;
667 }
668
669 $trusted_devices = get_user_meta($user_id, 'tfa_trusted_devices', true);
670
671 if (!is_array($trusted_devices)) $trusted_devices = array();
672
673 return $trusted_devices;
674 }
675
676 /**
677 * Trust the current device
678 *
679 * @param Integer $user_id - WordPress user ID
680 * @param Integer $trusted_for - time to trust for, in days
681 */
682 public function trust_device($user_id, $trusted_for) {
683
684 $trusted_devices = $this->user_get_trusted_devices($user_id);
685
686 $time_now = time();
687
688 foreach ($trusted_devices as $k => $device) {
689 if (empty($device['until']) || $device['until'] <= $time_now) unset($trusted_devices[$k]);
690 }
691
692 $until = $time_now + $trusted_for * 86400;
693
694 $token = bin2hex($this->random_bytes(40));
695
696 $trusted_devices[] = array(
697 'ip' => $_SERVER['REMOTE_ADDR'],
698 'until' => $until,
699 'user_agent' => empty($_SERVER['HTTP_USER_AGENT']) ? '' : (string) $_SERVER['HTTP_USER_AGENT'],
700 'token' => $token
701 );
702
703 $this->user_set_trusted_devices($user_id, $trusted_devices);
704
705 $this->set_cookie('simbatfa_trust_token', $token, $until);
706 }
707
708 /**
709 * Returns true if running on a PHP version on which mcrypt has been deprecated
710 *
711 * @return Boolean
712 */
713 public function is_mcrypt_deprecated() {
714 return (7 == PHP_MAJOR_VERSION && PHP_MINOR_VERSION >= 1);
715 }
716
717 /**
718 * Return the specified number of bytes
719 *
720 * @param Integer $bytes
721 *
722 * @throws Exception
723 *
724 * @return String
725 */
726 public function random_bytes($bytes) {
727 if (function_exists('random_bytes')) {
728 return random_bytes($bytes);
729 } elseif (function_exists('mcrypt_create_iv')) {
730 return $this->is_mcrypt_deprecated() ? @mcrypt_create_iv($bytes, MCRYPT_RAND) : mcrypt_create_iv($bytes, MCRYPT_RAND);
731 } elseif (function_exists('openssl_random_pseudo_bytes')) {
732 return openssl_random_pseudo_bytes($bytes);
733 }
734 throw new Exception('One of the mcrypt or openssl PHP modules needs to be installed');
735 }
736
737 /**
738 * Set a cookie so that, however we logged in, it can be found
739 *
740 * @param String $name - the cookie name
741 * @param String $value - the cookie value
742 * @param Integer $expires - when the cookie expires, in epoch time. Defaults to 24 hours' time. Values in the past cause cookie deletion.
743 */
744 protected function set_cookie($name, $value, $expires = null) {
745 if (null === $expires) $expires = time() + 86400;
746 $secure = is_ssl();
747 $secure_logged_in_cookie = ($secure && 'https' === parse_url(get_option('home'), PHP_URL_SCHEME));
748 $secure = apply_filters('secure_auth_cookie', $secure, get_current_user_id());
749 $secure_logged_in_cookie = apply_filters('secure_logged_in_cookie', $secure_logged_in_cookie, get_current_user_id(), $secure);
750
751 setcookie($name, $value, $expires, ADMIN_COOKIE_PATH, COOKIE_DOMAIN, $secure, true);
752 setcookie($name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true);
753 if (COOKIEPATH != SITECOOKIEPATH) {
754 setcookie($name, $value, $expires, SITECOOKIEPATH, COOKIE_DOMAIN, $secure_logged_in_cookie, true);
755 }
756 }
757
758 /**
759 * Get a list of trusted devices for the user
760 *
761 * @param Integer $user_id - WordPress user ID
762 * @param Array $trusted_devices - the list of devices
763 */
764 public function user_set_trusted_devices($user_id, $trusted_devices) {
765 update_user_meta($user_id, 'tfa_trusted_devices', $trusted_devices);
766 }
767
768 /**
769 * Get the user capability needed for managing TFA users.
770 * 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)
771 *
772 * @return String
773 */
774 public function get_management_capability() {
775 return apply_filters('simba_tfa_management_capability', 'manage_options');
776 }
777
778 /**
779 * Used with set_error_handler()
780 *
781 * @param Integer $errno
782 * @param String $errstr
783 * @param String $errfile
784 * @param Integer $errline
785 *
786 * @return Boolean
787 */
788 public function get_php_errors($errno, $errstr, $errfile, $errline) {
789 if (0 == error_reporting()) return true;
790 $logline = $this->php_error_to_logline($errno, $errstr, $errfile, $errline);
791 $this->logged[] = $logline;
792 # Don't pass it up the chain (since it's going to be output to the user always)
793 return true;
794 }
795
796 public function php_error_to_logline($errno, $errstr, $errfile, $errline) {
797 switch ($errno) {
798 case 1: $e_type = 'E_ERROR'; break;
799 case 2: $e_type = 'E_WARNING'; break;
800 case 4: $e_type = 'E_PARSE'; break;
801 case 8: $e_type = 'E_NOTICE'; break;
802 case 16: $e_type = 'E_CORE_ERROR'; break;
803 case 32: $e_type = 'E_CORE_WARNING'; break;
804 case 64: $e_type = 'E_COMPILE_ERROR'; break;
805 case 128: $e_type = 'E_COMPILE_WARNING'; break;
806 case 256: $e_type = 'E_USER_ERROR'; break;
807 case 512: $e_type = 'E_USER_WARNING'; break;
808 case 1024: $e_type = 'E_USER_NOTICE'; break;
809 case 2048: $e_type = 'E_STRICT'; break;
810 case 4096: $e_type = 'E_RECOVERABLE_ERROR'; break;
811 case 8192: $e_type = 'E_DEPRECATED'; break;
812 case 16384: $e_type = 'E_USER_DEPRECATED'; break;
813 case 30719: $e_type = 'E_ALL'; break;
814 default: $e_type = "E_UNKNOWN ($errno)"; break;
815 }
816
817 if (!is_string($errstr)) $errstr = serialize($errstr);
818
819 if (0 === strpos($errfile, ABSPATH)) $errfile = substr($errfile, strlen(ABSPATH));
820
821 return "PHP event: code $e_type: $errstr (line $errline, $errfile)";
822
823 }
824
825 /**
826 * Runs upon the WordPress 'init' action
827 */
828 public function init() {
829 if ((!is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) && is_user_logged_in() && file_exists($this->includes_dir().'/tfa_frontend.php')) {
830 $this->load_frontend();
831 } else {
832 add_shortcode('twofactor_user_settings', array($this, 'shortcode_when_not_logged_in'));
833 }
834 }
835
836 /**
837 * Return the TOTP provider object.
838 *
839 * @param String $controller_id - which controller
840 *
841 * @return Simba_TFA_Provider_totp
842 */
843 public function get_controller($controller_id = 'totp') {
844 return $this->controllers[$controller_id];
845 }
846
847 /**
848 * Return all OTP controllers
849 *
850 * @return Array
851 */
852 public function get_controllers() {
853 return $this->controllers;
854 }
855
856 /**
857 * Deprecated synonym for get_controller('totp')
858 *
859 * @return Simba_TFA_Provider_totp
860 */
861 public function get_totp_controller() {
862 trigger_error("Deprecated: Call get_controller('totp'), not get_totp_controller()", E_USER_WARNING);
863 return $this->get_controller('totp');
864 }
865
866 /**
867 * "Shared" - i.e. could be called from either front-end or back-end
868 */
869 public function shared_ajax() {
870
871 if (empty($_POST['subaction']) || empty($_POST['nonce']) || !is_user_logged_in() || !wp_verify_nonce($_POST['nonce'], 'tfa_shared_nonce')) die('Security check (3).');
872
873 global $current_user;
874
875 $subaction = $_POST['subaction'];
876
877 if ('refreshotp' == $subaction) {
878
879 $code = $this->get_controller('totp')->get_current_code($current_user->ID);
880
881 if (false === $code) die(json_encode(array('code' => '')));
882
883 die(json_encode(array('code' => $code)));
884
885 } elseif ('untrust_device' == $subaction && isset($_POST['device_id'])) {
886 $this->untrust_device(stripslashes($_POST['device_id']));
887 ob_start();
888 $this->include_template('trusted-devices-inner-box.php', array('trusted_devices' => $this->user_get_trusted_devices()));
889 echo json_encode(array('trusted_list' => ob_get_clean()));
890 }
891
892 exit;
893
894 }
895
896 /**
897 * Mark a device as untrusted for the current user
898 *
899 * @param String $device_id
900 */
901 protected function untrust_device($device_id) {
902
903 $trusted_devices = $this->user_get_trusted_devices();
904
905 unset($trusted_devices[$device_id]);
906
907 global $current_user;
908 $current_user_id = $current_user->ID;
909
910 $this->user_set_trusted_devices($current_user_id, $trusted_devices);
911
912 }
913
914 /**
915 * Called upon the AJAX action simbatfa-init-otp . Will die.
916 *
917 * Uses these keys from $_POST: user
918 */
919 public function tfaInitLogin() {
920
921 if (empty($_POST['user'])) die('Security check (2).');
922
923 if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) {
924 $res = array('result' => false, 'user_can_trust' => false);
925 } else {
926
927 if (!function_exists('sanitize_user')) require_once ABSPATH.WPINC.'/formatting.php';
928
929 // WP's password-checking sanitizes the supplied user, so we must do the same to check if TFA is enabled for them
930 $auth_info = array('log' => sanitize_user(stripslashes((string)$_POST['user'])));
931
932 if (!empty($_COOKIE['simbatfa_trust_token'])) $auth_info['trust_token'] = (string) $_COOKIE['simbatfa_trust_token'];
933
934 $res = $this->pre_auth($auth_info, 'array');
935 }
936
937 $results = array(
938 'jsonstarter' => 'justhere',
939 'status' => $res['result'],
940 );
941
942 if (!empty($res['user_can_trust'])) {
943 $results['user_can_trust'] = 1;
944 if (!empty($res['user_already_trusted'])) $results['user_already_trusted'] = 1;
945 }
946
947
948 if (!empty($this->output_buffering)) {
949 if (!empty($this->logged)) {
950 $results['php_output'] = $this->logged;
951 }
952 restore_error_handler();
953 $buffered = ob_get_clean();
954 if ($buffered) $results['extra_output'] = $buffered;
955 }
956
957 $results = apply_filters('simbatfa_check_tfa_requirements_ajax_response', $results);
958
959 echo json_encode($results);
960
961 exit;
962 }
963
964 /**
965 * Enable or disable TFA for a user
966 *
967 * @param Integer $user_id - the WordPress user ID
968 * @param String $setting - either "true" (to turn on) or "false" (to turn off)
969 */
970 public function change_tfa_enabled_status($user_id, $setting) {
971 $previously_enabled = $this->is_activated_by_user($user_id) ? 1 : 0;
972 $setting = ('true' === $setting) ? 1 : 0;
973 update_user_meta($user_id, 'tfa_enable_tfa', $setting);
974 do_action('simba_tfa_activation_status_saved', $user_id, $setting, $previously_enabled, $this);
975 }
976
977 /**
978 * Here's where the login action happens. Called on the WP 'authenticate' action (which also happens when wp-login.php loads, so parameters need checking).
979 *
980 * @param WP_Error|WP_User $user
981 * @param String $username - this is not necessarily the WP username; it is whatever was typed in the form, so can be an email address
982 * @param String $password
983 *
984 * @return WP_Error|WP_User
985 */
986 public function tfaVerifyCodeAndUser($user, $username, $password) {
987
988 // Do not require a TFA code when authenticating via cookie (or other non-login-form mechanism)
989 if ('' === $username && is_multisite()) return $user;
990
991 // When both the AIOWPS and Two Factor Authentication plugins are active, this function is called more than once; that should be short-circuited.
992 if (isset(self::$is_authenticated[$this->authentication_slug]) && self::$is_authenticated[$this->authentication_slug]) {
993 return $user;
994 }
995
996 $original_user = $user;
997 $params = stripslashes_deep($_POST);
998
999 // 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
1000 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))) {
1001 // This forces a new password authentication below
1002 $user = false;
1003 }
1004
1005 if (is_wp_error($user)) {
1006 $ret = $user;
1007 } else {
1008
1009 if (is_object($user) && isset($user->ID) && isset($user->user_login)) {
1010 $params['log'] = $user->user_login;
1011 // Confirm that this is definitely a username regardless of its format
1012 $may_be_email = false;
1013 } else {
1014 $params['log'] = $username;
1015 $may_be_email = true;
1016 }
1017
1018 $params['caller'] = $_SERVER['PHP_SELF'] ? $_SERVER['PHP_SELF'] : $_SERVER['REQUEST_URI'];
1019 if (!empty($_COOKIE['simbatfa_trust_token'])) $params['trust_token'] = (string) $_COOKIE['simbatfa_trust_token'];
1020
1021 if (isset($from_password) && false !== $from_password) {
1022 // Support login forms that can't be hooked via appending to the password
1023 $speculatively_try_appendage = true;
1024 $params['two_factor_code'] = $from_password['tfa_code'];
1025 }
1026
1027 $code_ok = $this->authorise_user_from_login($params, $may_be_email);
1028
1029 if (is_wp_error($code_ok)) {
1030 $ret = $code_ok;
1031 } elseif (!$code_ok) {
1032 $encryption_enabled = $this->get_option('tfa_encrypt_secrets');
1033 $additional = ($encryption_enabled && (!defined('SIMBA_TFA_DB_ENCRYPTION_KEY') || '' === SIMBA_TFA_DB_ENCRYPTION_KEY)) ? ' ' . htmlspecialchars(__('The "encrypt secrets" feature is currently enabled, but no encryption key has been found (set via the SIMBA_TFA_DB_ENCRYPTION_KEY constant).', 'two-factor-authentication').' '.__('This indicates that either setup failed, or your WordPress installation has been corrupted.', 'two-factor-authentication')) . ' <a href="' . esc_url($this->get_faq_url()) . '">'. __('Go here for the FAQs, which explain how a website owner can de-activate the plugin without needing to login.', 'two-factor-authentication') .'</a>' : '';
1034 $ret = new WP_Error('authentication_failed', '<strong>'.__('Error:', 'two-factor-authentication').'</strong> '.apply_filters('simba_tfa_message_code_incorrect', __('The one-time password (TFA code) you entered was incorrect.', 'two-factor-authentication') . $additional));
1035 $this->log_incorrect_tfa_code_attempt($user);
1036 } elseif ($user) {
1037 $ret = $user;
1038 } else {
1039
1040 if (!empty($speculatively_try_appendage) && true === $code_ok) {
1041 $password = $from_password['password'];
1042 }
1043
1044 $username_is_email = false;
1045
1046 if (function_exists('wp_authenticate_username_password') && $may_be_email && filter_var($username, FILTER_VALIDATE_EMAIL)) {
1047 global $wpdb;
1048 // This has to match self::authorise_user_from_login()
1049 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $username));
1050 if (is_object($response)) $username_is_email = true;
1051 }
1052
1053 $ret = $username_is_email ? wp_authenticate_email_password(null, $username, $password) : wp_authenticate_username_password(null, $username, $password);
1054 }
1055
1056 }
1057
1058 $ret = apply_filters('simbatfa_verify_code_and_user_result', $ret, $original_user, $username, $password);
1059
1060 // If the TFA code was actually validated (not just not required, for example), then $code_ok is (boolean)true
1061 if (isset($code_ok) && true === $code_ok && is_a($ret, 'WP_User')) {
1062 // Though $_SERVER['SERVER_NAME'] can't always be trusted (if the webserver is misconfigured), anyone using this already has password and TFA clearance.
1063 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'] || preg_match('/\.localdomain$/', $_SERVER['SERVER_NAME']))))) {
1064
1065 $trusted_for = $this->get_option('tfa_trusted_for');
1066 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1067
1068 $this->trust_device($ret->ID, $trusted_for);
1069 }
1070 }
1071
1072 self::$is_authenticated[$this->authentication_slug] = true;
1073
1074 return $ret;
1075 }
1076
1077 /**
1078 * Save incorrect TFA code attempts in database
1079 *
1080 * @param Array $tfa_incorrect_code_attempts - all user info with incorrent code attempts
1081 * @param Boolean $udpate - update in option table
1082 *
1083 * @retrun Void
1084 */
1085 private function save_incorrect_tfa_code_attempts($tfa_incorrect_code_attempts, $update = false) {
1086 if ($update) {
1087 update_site_option('tfa_incorrect_code_attempts', $tfa_incorrect_code_attempts);
1088 } else {
1089 add_site_option('tfa_incorrect_code_attempts', $tfa_incorrect_code_attempts);
1090 }
1091 }
1092
1093 /**
1094 * Remove old incorrect TFA code attempts
1095 *
1096 * @param Array $user_info - user invalid attempts
1097 *
1098 * @retrun Array
1099 */
1100 private function remove_incorrect_tfa_code_old_attempts($user_info) {
1101 $splice_recs = 0;
1102 foreach ($user_info['attempts'] as $attempt) {
1103 $mins_diff = (time() - $attempt['activity_time']) / 60;
1104 if ($mins_diff >= TFA_INCORRECT_ATTEMPTS_WITHIN_MINUTES_LIMIT) {
1105 $splice_recs++;
1106 }
1107 }
1108 if ($splice_recs > 0) array_splice($user_info['attempts'], 0, $splice_recs); // remove all older attempts.
1109 return $user_info;
1110 }
1111
1112 /**
1113 * Log incorrect TFA code attempt and email user if atttemp exceeded limit
1114 *
1115 * @param WP_User $user - logging in user object
1116 *
1117 * @retrun Void
1118 */
1119 private function log_incorrect_tfa_code_attempt($user) {
1120 $tfa_incorrect_code_attempts = get_site_option('tfa_incorrect_code_attempts');
1121 if (empty($tfa_incorrect_code_attempts)) $tfa_incorrect_code_attempts = array();
1122 $userinfo_added = false;
1123 $update = false;
1124 if (count($tfa_incorrect_code_attempts) > 0) {
1125 foreach ($tfa_incorrect_code_attempts as $i => $user_info) {
1126 $user_info = $this->remove_incorrect_tfa_code_old_attempts($user_info); // remove old (before 30 mins) incorrect tfa code attempts by users
1127 if ($user_info['username'] == $user->user_login) {
1128 $userinfo_added = true;
1129 if (count($user_info['attempts']) >= TFA_INCORRECT_MAX_ATTEMPTS_ALLOWED_LIMIT && empty($user_info['mailsent'])) {
1130 $this->notify_incorrect_tfa_code_attempts($user_info, $user->user_email); // if incorrect tfa attempts are more than max allowed notify user by email that some one else has your password.
1131 $user_info['mailsent'] = 1;
1132 } else {
1133 if (0 == count($user_info['attempts'])) $user_info['mailsent'] = 0;
1134 $user_info['attempts'][] = $this->get_incorrect_tfa_attempt_info(); //add new incorrect attempt for existing user.
1135 }
1136 }
1137 $tfa_incorrect_code_attempts[$i] = $user_info;
1138 }
1139 $update = true;
1140 }
1141 if (false == $userinfo_added) {
1142 $tfa_incorrect_code_attempts[] = $this->get_incorrect_tfa_user_info($user); //add incorrect attempt with username etc info.
1143 }
1144 $this->save_incorrect_tfa_code_attempts($tfa_incorrect_code_attempts, $update);
1145 }
1146
1147 /**
1148 * Get incorrect attempt info time and IP address to save in database
1149 *
1150 * @retrun Array
1151 */
1152 private function get_incorrect_tfa_attempt_info() {
1153 $ip_address = apply_filters('tfa_user_ip_address', $_SERVER['REMOTE_ADDR']);
1154 return array('activity_time' => time(), 'ip_address' => $ip_address);
1155 }
1156
1157 /**
1158 * Get incorrect attempt with userinfo to save in database
1159 *
1160 * @param WP_User $user - logging in user object
1161 *
1162 * @retrun Array
1163 */
1164 private function get_incorrect_tfa_user_info($user) {
1165 return array('username' => $user->user_login, 'attempts' => array($this->get_incorrect_tfa_attempt_info()));
1166 }
1167
1168 /**
1169 * Notify user might be someone else has your possword
1170 *
1171 * @param Array $user_info - user's incorrect attempt informaion
1172 * @param String $user_email - user email address notification to be sent.
1173 */
1174 private function notify_incorrect_tfa_code_attempts($user_info, $user_email) {
1175 $subject = __('Incorrect TFA code attempts', 'two-factor-authentication');
1176 $email_msg = sprintf(__('There has been an incorrect TFA code entered for logging in to your account %s.', 'two-factor-authentication'), $user_info['username']) . "\n\n" .
1177 __('Attempts', 'two-factor-authentication') . "\n\n";
1178 foreach ($user_info['attempts'] as $index => $attempt) {
1179 $email_msg.= ($index+1) . '. ' . wp_date('F j, Y g:i a', $attempt['activity_time'], wp_timezone()) . ' ' . __('from', 'two-factor-authentication') . ' ' . $attempt['ip_address']. "\n";
1180 }
1181 $email_msg.= "\n" . __('If the above attempts were not by you then someone else has your password.', 'two-factor-authentication') . "\n" .
1182 __('TFA codes are checked only after the password has been successfully checked.', 'two-factor-authentication') . "\n\n" .
1183 __('Please change your password urgently.', 'two-factor-authentication') . "\n";
1184 $mail_sent = wp_mail($user_email, $subject, $email_msg);
1185 }
1186
1187 // N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
1188 public function user_can_trust($user_id) {
1189 // 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
1190 return apply_filters('simba_tfa_user_can_trust', false, $user_id);
1191 }
1192
1193 /**
1194 * Should the user be asked for a TFA code? And optionally, is the user allowed to trust devices?
1195 *
1196 * @param Array $params - the key used is 'log', indicating the username or email address
1197 * @param String $response_format - 'simple' (historic format) or 'array' (richer info)
1198 *
1199 * @return Boolean
1200 */
1201 public function pre_auth($params, $response_format = 'simple') {
1202 global $wpdb;
1203
1204 $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']);
1205 $user = $wpdb->get_row($query);
1206
1207 if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
1208 // Corner-case: login looks like an email, but is a username rather than email address
1209 $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']));
1210 }
1211
1212 $is_activated_for_user = true;
1213 $is_activated_by_user = false;
1214
1215 $result = false;
1216
1217 $totp_controller = $this->get_controller('totp');
1218
1219 if ($user) {
1220 $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true);
1221 $is_activated_for_user = $this->is_activated_for_user($user->ID);
1222 $is_activated_by_user = $this->is_activated_by_user($user->ID);
1223
1224 if ($is_activated_for_user && $is_activated_by_user) {
1225
1226 // No private key yet, generate one. This shouldn't really be possible.
1227 if (!$tfa_priv_key) $tfa_priv_key = $totp_controller->addPrivateKey($user->ID);
1228
1229 $code = $totp_controller->generateOTP($user->ID, $tfa_priv_key);
1230
1231 $result = true;
1232 }
1233 }
1234
1235 if ('array' != $response_format) return $result;
1236
1237 $ret = array('result' => $result);
1238
1239 if ($result) {
1240 $ret['user_can_trust'] = $this->user_can_trust($user->ID);
1241 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user->ID, $params['trust_token'])) {
1242 $ret['user_already_trusted'] = 1;
1243 }
1244 }
1245
1246 return $ret;
1247 }
1248
1249 /**
1250 * Print the radio buttons for enabling/disabling TFA
1251 *
1252 * @param Integer $user_id - the WordPress user ID
1253 * @param Boolean $long_label - whether to use a long label rather than a short one
1254 * @param String $style - valid values are "show_current" and "require_current"
1255 */
1256 public function paint_enable_tfa_radios($user_id, $long_label = false, $style = 'show_current') {
1257
1258 if (!$user_id) return;
1259
1260 if ('require_current' != $style) $style = 'show_current';
1261
1262 $is_required = $this->is_required_for_user($user_id);
1263 $is_activated = $this->is_activated_by_user($user_id);
1264
1265 if ($is_required) {
1266 $require_after = absint($this->get_option('tfa_requireafter'));
1267 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>';
1268 }
1269
1270 $tfa_enabled_label = $long_label ? __('Enable two-factor authentication', 'two-factor-authentication') : __('Enabled', 'two-factor-authentication');
1271
1272 if ('show_current' == $style) {
1273 $tfa_enabled_label .= ' '.sprintf(__('(Current code: %s)', 'two-factor-authentication'), $this->get_controller('totp')->current_otp_code($user_id));
1274 } elseif ('require_current' == $style) {
1275 $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">');
1276 }
1277
1278 $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;
1279
1280 $tfa_disabled_label = $long_label ? __('Disable two-factor authentication', 'two-factor-authentication') : __('Disabled', 'two-factor-authentication');
1281
1282 if ('require_current' == $style) echo '<input type="hidden" name="require_current" value="1">'."\n";
1283
1284 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>';
1285
1286 // 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
1287 // 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.
1288 if ($show_disable) {
1289 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>';
1290 }
1291 }
1292
1293 /**
1294 * Retrieve a saved option
1295 *
1296 * @param String $key - option key
1297 *
1298 * @return Mixed
1299 */
1300 public function get_option($key) {
1301 if (!is_multisite()) return get_option($key);
1302 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1303 $get_option_site_id = apply_filters('simba_tfa_get_option_site_id', $main_site_id);
1304 switch_to_blog($get_option_site_id);
1305 $value = get_option($key);
1306 restore_current_blog();
1307 return $value;
1308 }
1309
1310 /**
1311 * Updates an option.
1312 *
1313 * @param String $key - option key
1314 * @param Mixed $value - option value
1315 *
1316 * @return Boolean
1317 */
1318 public function update_option($key, $value) {
1319 if (!is_multisite()) return update_option($key, $value);
1320
1321 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1322 $update_option_site_id = apply_filters('simba_tfa_update_option_site_id', $main_site_id);
1323
1324 switch_to_blog($update_option_site_id);
1325 $result = update_option($key, $value);
1326 restore_current_blog();
1327
1328 return $result;
1329 }
1330
1331 /**
1332 * Deletes an option.
1333 *
1334 * @param String $key - option key
1335 *
1336 * @return Boolean
1337 */
1338 public function delete_option($key) {
1339 if (!is_multisite()) return delete_option($key);
1340
1341 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1342 $delete_option_site_id = apply_filters('simba_tfa_delete_option_site_id', $main_site_id);
1343
1344 switch_to_blog($delete_option_site_id);
1345 $result = delete_option($key);
1346 restore_current_blog();
1347
1348 return $result;
1349 }
1350
1351 /**
1352 * Paint a list of checkboxes, one for each role
1353 *
1354 * @param String $prefix
1355 * @param Integer $default - default value (0 or 1)
1356 */
1357 public function list_user_roles_checkboxes($prefix = '', $default = 1) {
1358 if (is_multisite()) {
1359 // Not a real WP role; needs separate handling
1360 $id = '_super_admin';
1361 $name = __('Multisite Super Admin', 'two-factor-authentication');
1362 $setting = $this->get_option('tfa_'.$prefix.$id);
1363 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
1364
1365 echo '<input type="checkbox" id="tfa_'.$prefix.$id.'" name="tfa_'.$prefix.$id.'" class="tfa_'.$prefix.'user_roles" value="1" '.($setting ? 'checked="checked"' :'').'> <label for="tfa_'.$prefix.$id.'">'.htmlspecialchars($name)."</label><br>\n";
1366 }
1367
1368 global $wp_roles;
1369 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
1370
1371 foreach ($wp_roles->role_names as $id => $name) {
1372 $setting = $this->get_option('tfa_'.$prefix.$id);
1373 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
1374
1375 echo '<input type="checkbox" id="tfa_'.$prefix.$id.'" name="tfa_'.$prefix.$id.'" class="tfa_'.$prefix.'user_roles" value="1" '.($setting ? 'checked="checked"' :'').'> <label for="tfa_'.$prefix.$id.'">'.htmlspecialchars(translate_user_role($name))."</label><br>\n";
1376 }
1377
1378 }
1379
1380 public function tfa_list_xmlrpc_status_radios() {
1381
1382 $setting = $this->get_option('tfa_xmlrpc_on');
1383 $setting = $setting ? 1 : 0;
1384
1385 $types = array(
1386 '0' => __('Do not require 2FA over XMLRPC (best option if you must use XMLRPC and your client does not support 2FA)', 'two-factor-authentication'),
1387 '1' => __('Do require 2FA over XMLRPC (best option if you do not use XMLRPC or are unsure)', 'two-factor-authentication')
1388 );
1389
1390 foreach($types as $id => $name) {
1391 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";
1392 }
1393 }
1394
1395 protected function is_caller_active() {
1396
1397 if (!defined('XMLRPC_REQUEST') || !XMLRPC_REQUEST) return true;
1398
1399 $saved_data = $this->get_option('tfa_xmlrpc_on');
1400
1401 return $saved_data ? true : false;
1402
1403 }
1404
1405 /**
1406 * @param Array $params
1407 * @param Boolean $may_be_email
1408 *
1409 * @return WP_Error|Boolean|Integer - WP_Error or false means failure; true or 1 means success, but true means the TFA code was validated
1410 */
1411 public function authorise_user_from_login($params, $may_be_email = false) {
1412
1413 $params = apply_filters('simbatfa_auth_user_from_login_params', $params);
1414
1415 global $wpdb;
1416
1417 if (!$this->is_caller_active()) return 1;
1418
1419 $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']);
1420 $response = $wpdb->get_row($query);
1421
1422 if (!$response && $may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
1423 // Corner-case: login looks like an email, but is a username rather than email address
1424 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']));
1425 }
1426
1427 $user_id = is_object($response) ? $response->ID : false;
1428 $user_registered = is_object($response) ? $response->user_registered : false;
1429
1430 $user_code = isset($params['two_factor_code']) ? str_replace(' ', '', trim($params['two_factor_code'])) : '';
1431
1432 // This condition in theory should not be possible
1433 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')));
1434
1435 if (!$this->is_activated_for_user($user_id)) return 1;
1436
1437 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user_id, $params['trust_token'])) {
1438 return 1;
1439 }
1440
1441 if (!$this->is_activated_by_user($user_id)) {
1442
1443 if (!$this->is_required_for_user($user_id)) return 1;
1444
1445 $enforce_require_after_check = true;
1446
1447 $require_enforce_after = $this->get_option('tfa_require_enforce_after');
1448
1449 // Don't enforce if the setting has never been saved
1450 if (is_string($require_enforce_after) && preg_match('#^(\d+)-(\d+)-(\d+)$#', $require_enforce_after, $enforce_matches)) {
1451
1452 // wp_date() is WP 5.3+, but performs translation into the site locale
1453 $current_date = function_exists('wp_date') ? wp_date('Y-m-d') : get_date_from_gmt(gmdate('Y-m-d H:i:s'), 'Y-m-d');
1454
1455 if (preg_match('#^(\d+)-(\d+)-(\d+)$#', $current_date, $current_date_matches)) {
1456 if ($current_date_matches[0] < $enforce_matches[0] || ($current_date_matches[0] == $enforce_matches[0] && ($current_date_matches[1] < $enforce_matches[1] || ($current_date_matches[1] == $enforce_matches[1] && $current_date_matches[2] < $enforce_matches[2])))) {
1457 // Enforcement not yet begun; skip
1458 $enforce_require_after_check = false;
1459 }
1460 }
1461
1462 }
1463
1464 $require_after = absint($this->get_option('tfa_requireafter')) * 86400;
1465
1466 $account_age = time() - strtotime($user_registered);
1467
1468 if ($account_age > $require_after && apply_filters('simbatfa_enforce_require_after_check', $enforce_require_after_check, $user_id, $require_after, $account_age)) {
1469
1470 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')));
1471 }
1472
1473 return 1;
1474 }
1475
1476 $tfa_creds_user_id = !empty($params['creds_user_id']) ? $params['creds_user_id'] : $user_id;
1477
1478 if ($tfa_creds_user_id != $user_id) {
1479
1480 // Authenticating using a different user's credentials (e.g. https://wordpress.org/plugins/use-administrator-password/)
1481 // In this case, we require that different user to have TFA active - so that this mechanism can't be used to avoid TFA
1482
1483 if (!$this->is_activated_for_user($tfa_creds_user_id) || !$this->is_activated_by_user($tfa_creds_user_id)) {
1484 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')));
1485 }
1486
1487 }
1488
1489 return $this->get_controller('totp')->check_code_for_user($tfa_creds_user_id, $user_code);
1490
1491 }
1492
1493 /**
1494 * Evaluate whether a trust token is valid for a user
1495 *
1496 * @param Integer $user_id - WP user ID
1497 * @param String $trust_token - trust token
1498 *
1499 * @return Boolean
1500 */
1501 protected function user_trust_token_valid($user_id, $trust_token) {
1502
1503 if (!is_string($trust_token) || strlen($trust_token) < 30) return false;
1504
1505 $trusted_devices = $this->user_get_trusted_devices($user_id);
1506
1507 $time_now = time();
1508
1509 foreach ($trusted_devices as $device) {
1510 if (empty($device['until']) || $device['until'] <= $time_now) continue;
1511 if (!empty($device['token']) && $device['token'] === $trust_token) {
1512 return true;
1513 }
1514 }
1515
1516 return false;
1517 }
1518
1519 /**
1520 * 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.
1521 *
1522 * @return String
1523 */
1524 protected function get_ajax_url() {
1525 $ajax_url = admin_url('admin-ajax.php');
1526 $parsed_url = parse_url($ajax_url);
1527 if (strtolower($parsed_url['host']) !== strtolower($_SERVER['HTTP_HOST']) && !empty($parsed_url['path'])) {
1528 // Mismatch - return the relative URL only
1529 $ajax_url = $parsed_url['path'];
1530 }
1531 return $ajax_url;
1532 }
1533
1534 /**
1535 * 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.
1536 */
1537 public function login_enqueue_scripts() {
1538 if (!$this->should_enqueue_login_scripts()) {
1539 return;
1540 }
1541
1542 if (isset($_GET['action']) && 'logout ' != $_GET['action'] && 'login' != $_GET['action']) return;
1543
1544 static $already_done = false;
1545 if ($already_done) return;
1546 $already_done = true;
1547
1548 // Prevent caching when in debug mode
1549 $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/tfa.js');
1550
1551 wp_enqueue_script('tfa-ajax-request', $this->includes_url().'/tfa.js', array('jquery'), $script_ver);
1552
1553 $trusted_for = $this->get_option('tfa_trusted_for');
1554 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1555
1556 $localize = array(
1557 'ajaxurl' => $this->get_ajax_url(),
1558 'click_to_enter_otp' => __("Click to enter One Time Password", 'two-factor-authentication'),
1559 'enter_username_first' => __('You have to enter a username first.', 'two-factor-authentication'),
1560 'otp' => __('One Time Password (i.e. 2FA)', 'two-factor-authentication'),
1561 'otp_login_help' => __('(check your OTP app to get this password)', 'two-factor-authentication'),
1562 '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),
1563 'is_trusted' => __('(Trusted device - no OTP code required)', 'two-factor-authentication'),
1564 'nonce' => wp_create_nonce('simba_tfa_loginform_nonce'),
1565 'login_form_selectors' => '',
1566 'login_form_off_selectors' => '',
1567 'error' => __('An error has occurred. Site owners can check the JavaScript console for more details.', 'two-factor-authentication'),
1568 );
1569
1570 // Spinner exists since WC 3.8. Use the proper functions to avoid SSL warnings.
1571 if (file_exists(ABSPATH.'wp-admin/images/spinner-2x.gif')) {
1572 $localize['spinnerimg'] = admin_url('images/spinner-2x.gif');
1573 } elseif (file_exists(ABSPATH.WPINC.'/images/spinner-2x.gif')) {
1574 $localize['spinnerimg'] = includes_url('images/spinner-2x.gif');
1575 }
1576
1577 $localize = apply_filters('simba_tfa_login_enqueue_localize', $localize);
1578
1579 wp_localize_script('tfa-ajax-request', 'simba_tfasettings', $localize);
1580
1581 }
1582
1583 /**
1584 * Check whether TFA login scripts should be enqueued or not.
1585 *
1586 * @return boolean True if the TFA login script should be enqueued, otherwise false.
1587 */
1588 private function should_enqueue_login_scripts() {
1589 if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) {
1590 return apply_filters('simbatfa_enqueue_login_scripts', false);
1591 }
1592
1593 global $wpdb;
1594 $sql = $wpdb->prepare('SELECT COUNT(user_id) FROM ' . $wpdb->usermeta . ' WHERE meta_key = %s AND meta_value = %d LIMIT 1', 'tfa_enable_tfa', 1);
1595 $count_user_id = $wpdb->get_var($sql);
1596
1597 if (is_null($count_user_id)) { // Error in query.
1598 return apply_filters('simbatfa_enqueue_login_scripts', true);
1599 } elseif ($count_user_id > 0) { // A user exists with TFA enabled.
1600 return apply_filters('simbatfa_enqueue_login_scripts', true);
1601 }
1602
1603 // No user exists with TFA enabled.
1604 return apply_filters('simbatfa_enqueue_login_scripts', false);
1605 }
1606
1607
1608 /**
1609 * Return or output view content
1610 *
1611 * @param String $path - path to template, usually relative to templates/ within the plugin directory
1612 * @param Array $extract_these - key/value pairs for substitution into the scope of the template
1613 * @param Boolean $return_instead_of_echo - what to do with the results
1614 *
1615 * @return String|Void
1616 */
1617 public function include_template($path, $extract_these = array(), $return_instead_of_echo = false) {
1618
1619 if ($return_instead_of_echo) ob_start();
1620
1621 $template_file = apply_filters('simatfa_template_file', $this->templates_dir().'/'.$path, $path, $extract_these, $return_instead_of_echo);
1622
1623 do_action('simbatfa_before_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1624
1625 if (!file_exists($template_file)) {
1626 error_log("TFA: template not found: $template_file (from $path)");
1627 echo __('Error:', 'two-factor-authentication').' '.__('Template path not found:', 'two-factor-authentication')." (".htmlspecialchars($path).")";
1628 } else {
1629 extract($extract_these);
1630 // The following are useful variables which can be used in the template.
1631 // They appear as unused, but may be used in the $template_file.
1632 $wpdb = $GLOBALS['wpdb'];// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wpdb might be used in the included template
1633 $simba_tfa = $this;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1634 $totp_controller = $this->get_controller('totp');// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1635 include $template_file;
1636 }
1637
1638 do_action('simbatfa_after_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1639
1640 if ($return_instead_of_echo) return ob_get_clean();
1641 }
1642
1643 /**
1644 * Make sure that self::$frontend is the instance of Simba_TFA_Frontend, and return it
1645 *
1646 * @return Simba_TFA_Frontend
1647 */
1648 public function load_frontend() {
1649 if (!class_exists('Simba_TFA_Frontend')) require_once($this->includes_dir().'/tfa_frontend.php');
1650 if (empty($this->frontend)) $this->frontend = new Simba_TFA_Frontend($this);
1651 return $this->frontend;
1652 }
1653
1654 // __return_empty_string() does not exist until WP 3.7
1655 public function shortcode_when_not_logged_in() {
1656 return '';
1657 }
1658
1659 /**
1660 * Set authentication slug.
1661 *
1662 * @param String $authentication_slug - Authentication slug. Verify that two-factor authentication should not be repeated for the same slug.
1663 */
1664 public function set_authentication_slug($authentication_slug) {
1665 $this->authentication_slug = $authentication_slug;
1666 }
1667
1668 }
1669