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

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