PluginProbe
Two Factor Authentication / 1.14.24
Two Factor Authentication v1.14.24
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.24, at simba-tfa/simba-tfa.php

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