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

1,723 lines 60.3 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 $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));
1066 if (is_a($user, 'WP_User')) $this->log_incorrect_tfa_code_attempt($user);
1067 } elseif ($user) {
1068 $ret = $user;
1069 } else {
1070
1071 if (!empty($speculatively_try_appendage) && true === $code_ok) {
1072 $password = $from_password['password'];
1073 }
1074
1075 $username_is_email = false;
1076
1077 if (function_exists('wp_authenticate_username_password') && $may_be_email && filter_var($username, FILTER_VALIDATE_EMAIL)) {
1078 global $wpdb;
1079 // This has to match self::authorise_user_from_login()
1080 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_email=%s", $username));
1081 if (is_object($response)) $username_is_email = true;
1082 }
1083
1084 $ret = $username_is_email ? wp_authenticate_email_password(null, $username, $password) : wp_authenticate_username_password(null, $username, $password);
1085 }
1086
1087 }
1088
1089 $ret = apply_filters('simbatfa_verify_code_and_user_result', $ret, $original_user, $username, $password);
1090
1091 // If the TFA code was actually validated (not just not required, for example), then $code_ok is (boolean)true
1092 if (isset($code_ok) && true === $code_ok && is_a($ret, 'WP_User')) {
1093 // Though $_SERVER['SERVER_NAME'] can't always be trusted (if the webserver is misconfigured), anyone using this already has password and TFA clearance.
1094 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']))))) {
1095
1096 $trusted_for = $this->get_option('tfa_trusted_for');
1097 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1098
1099 $this->trust_device($ret->ID, $trusted_for);
1100 }
1101 }
1102
1103 self::$is_authenticated[$this->authentication_slug] = true;
1104
1105 return $ret;
1106 }
1107
1108 /**
1109 * Save incorrect TFA code attempts in database
1110 *
1111 * @param Array $tfa_incorrect_code_attempts - all user info with incorrect code attempts
1112 * @param Boolean $update - update in option table
1113 *
1114 * @return Void
1115 */
1116 private function save_incorrect_tfa_code_attempts($tfa_incorrect_code_attempts, $update = false) {
1117 if ($update) {
1118 update_site_option('tfa_incorrect_code_attempts', $tfa_incorrect_code_attempts);
1119 } else {
1120 add_site_option('tfa_incorrect_code_attempts', $tfa_incorrect_code_attempts);
1121 }
1122 }
1123
1124 /**
1125 * Remove old incorrect TFA code attempts
1126 *
1127 * @param Array $user_info - user invalid attempts
1128 *
1129 * @return Array
1130 */
1131 private function remove_incorrect_tfa_code_old_attempts($user_info) {
1132 $splice_recs = 0;
1133 foreach ($user_info['attempts'] as $attempt) {
1134 $mins_diff = (time() - $attempt['activity_time']) / 60;
1135 if ($mins_diff >= TFA_INCORRECT_ATTEMPTS_WITHIN_MINUTES_LIMIT) {
1136 $splice_recs++;
1137 }
1138 }
1139 if ($splice_recs > 0) {
1140 array_splice($user_info['attempts'], 0, $splice_recs); // remove all older attempts.
1141 }
1142 return $user_info;
1143 }
1144
1145 /**
1146 * Log incorrect TFA code attempt and email user if attempt exceeded limit
1147 *
1148 * @param WP_User $user - user object for the user logging in
1149 *
1150 * @return Void
1151 */
1152 private function log_incorrect_tfa_code_attempt($user) {
1153 $tfa_incorrect_code_attempts = get_site_option('tfa_incorrect_code_attempts');
1154 if (empty($tfa_incorrect_code_attempts)) $tfa_incorrect_code_attempts = array();
1155 $userinfo_added = false;
1156 $update = false;
1157 if (count($tfa_incorrect_code_attempts) > 0) {
1158 foreach ($tfa_incorrect_code_attempts as $i => $user_info) {
1159 $user_info = $this->remove_incorrect_tfa_code_old_attempts($user_info); // remove old (before 30 mins) incorrect tfa code attempts by users
1160 if (empty($user_info['attempts'])) {
1161 unset($tfa_incorrect_code_attempts[$i]);
1162 continue;
1163 }
1164 if ($user_info['user_id'] == $user->ID) {
1165 $userinfo_added = true;
1166 if (count($user_info['attempts']) >= TFA_INCORRECT_MAX_ATTEMPTS_ALLOWED_LIMIT && empty($user_info['mailsent'])) {
1167 $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.
1168 $user_info['mailsent'] = 1;
1169 } else {
1170 if (0 == count($user_info['attempts'])) $user_info['mailsent'] = 0;
1171 $user_info['attempts'][] = $this->get_incorrect_tfa_attempt_info(); //add new incorrect attempt for existing user.
1172 }
1173 }
1174 $tfa_incorrect_code_attempts[$i] = $user_info;
1175 }
1176 $update = true;
1177 }
1178 if (false == $userinfo_added) {
1179 $tfa_incorrect_code_attempts[] = $this->get_incorrect_tfa_user_info($user); //add incorrect attempt with username etc info.
1180 }
1181 $this->save_incorrect_tfa_code_attempts($tfa_incorrect_code_attempts, $update);
1182 }
1183
1184 /**
1185 * Get incorrect attempt info time and IP address to save in database
1186 *
1187 * @return Array
1188 */
1189 private function get_incorrect_tfa_attempt_info() {
1190 $ip_address = apply_filters('tfa_user_ip_address', $_SERVER['REMOTE_ADDR']);
1191 return array('activity_time' => time(), 'ip_address' => $ip_address);
1192 }
1193
1194 /**
1195 * Get incorrect attempt with userinfo to save in database
1196 *
1197 * @param WP_User $user - logging in user object
1198 *
1199 * @return Array
1200 */
1201 private function get_incorrect_tfa_user_info($user) {
1202 return array('user_id' => $user->ID, 'attempts' => array($this->get_incorrect_tfa_attempt_info()));
1203 }
1204
1205 /**
1206 * Notify user might be someone else has your possword
1207 *
1208 * @param Array $user_info - user's incorrect attempt information
1209 * @param String $user_email - user email address notification to be sent.
1210 */
1211 private function notify_incorrect_tfa_code_attempts($user_info, $user_email) {
1212 $subject = __('Incorrect TFA code attempts', 'two-factor-authentication');
1213 $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" .
1214 __('Attempts', 'two-factor-authentication') . "\n\n";
1215 foreach ($user_info['attempts'] as $index => $attempt) {
1216 $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";
1217 }
1218 $email_msg.= "\n" . __('If the above attempts were not by you then someone else has your password.', 'two-factor-authentication') . "\n" .
1219 __('TFA codes are checked only after the password has been successfully checked.', 'two-factor-authentication') . "\n\n" .
1220 __('Please change your password urgently.', 'two-factor-authentication') . "\n";
1221 $mail_sent = wp_mail($user_email, $subject, $email_msg);
1222 }
1223
1224 // N.B. - This doesn't check is_activated_for_user() - the caller would normally want to do that first
1225 public function user_can_trust($user_id) {
1226 // 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
1227 return apply_filters('simba_tfa_user_can_trust', false, $user_id);
1228 }
1229
1230 /**
1231 * Should the user be asked for a TFA code? And optionally, is the user allowed to trust devices?
1232 *
1233 * @param Array $params - the key used is 'log', indicating the username or email address
1234 * @param String $response_format - 'simple' (historic format) or 'array' (richer info)
1235 *
1236 * @return Boolean
1237 */
1238 public function pre_auth($params, $response_format = 'simple') {
1239 global $wpdb;
1240
1241 $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']);
1242 $user = $wpdb->get_row($query);
1243
1244 if (!$user && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
1245 // Corner-case: login looks like an email, but is a username rather than email address
1246 $user = $wpdb->get_row($wpdb->prepare("SELECT ID, user_email from ".$wpdb->users." WHERE user_login=%s", $params['log']));
1247 }
1248
1249 $is_activated_for_user = true;
1250 $is_activated_by_user = false;
1251
1252 $result = false;
1253
1254 $totp_controller = $this->get_controller('totp');
1255
1256 if ($user) {
1257 $tfa_priv_key = get_user_meta($user->ID, 'tfa_priv_key_64', true);
1258 $is_activated_for_user = $this->is_activated_for_user($user->ID);
1259 $is_activated_by_user = $this->is_activated_by_user($user->ID);
1260
1261 if ($is_activated_for_user && $is_activated_by_user) {
1262
1263 // No private key yet, generate one. This shouldn't really be possible.
1264 if (!$tfa_priv_key) $tfa_priv_key = $totp_controller->addPrivateKey($user->ID);
1265
1266 $code = $totp_controller->generateOTP($user->ID, $tfa_priv_key);
1267
1268 $result = true;
1269 }
1270 }
1271
1272 if ('array' != $response_format) return $result;
1273
1274 $ret = array('result' => $result);
1275
1276 if ($result) {
1277 $ret['user_can_trust'] = $this->user_can_trust($user->ID);
1278 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user->ID, $params['trust_token'])) {
1279 $ret['user_already_trusted'] = 1;
1280 }
1281 }
1282
1283 return $ret;
1284 }
1285
1286 /**
1287 * Print the radio buttons for enabling/disabling TFA
1288 *
1289 * @param Integer $user_id - the WordPress user ID
1290 * @param Boolean $long_label - whether to use a long label rather than a short one
1291 * @param String $style - valid values are "show_current" and "require_current"
1292 */
1293 public function paint_enable_tfa_radios($user_id, $long_label = false, $style = 'show_current') {
1294
1295 if (!$user_id) return;
1296
1297 if ('require_current' != $style) $style = 'show_current';
1298
1299 $is_required = $this->is_required_for_user($user_id);
1300 $is_activated = $this->is_activated_by_user($user_id);
1301
1302 if ($is_required) {
1303 $require_after = absint($this->get_option('tfa_requireafter'));
1304 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>';
1305 }
1306
1307 $tfa_enabled_label = $long_label ? __('Enable two-factor authentication', 'two-factor-authentication') : __('Enabled', 'two-factor-authentication');
1308
1309 if ('show_current' == $style) {
1310 $tfa_enabled_label .= ' '.sprintf(__('(Current code: %s)', 'two-factor-authentication'), $this->get_controller('totp')->current_otp_code($user_id));
1311 } elseif ('require_current' == $style) {
1312 $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">');
1313 }
1314
1315 $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;
1316
1317 $tfa_disabled_label = $long_label ? __('Disable two-factor authentication', 'two-factor-authentication') : __('Disabled', 'two-factor-authentication');
1318
1319 if ('require_current' == $style) echo '<input type="hidden" name="require_current" value="1">'."\n";
1320
1321 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>';
1322
1323 // 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
1324 // 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.
1325 if ($show_disable) {
1326 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>';
1327 }
1328 }
1329
1330 /**
1331 * Retrieve a saved option
1332 *
1333 * @param String $key - option key
1334 *
1335 * @return Mixed
1336 */
1337 public function get_option($key) {
1338 if (!is_multisite()) return get_option($key);
1339 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1340 $get_option_site_id = apply_filters('simba_tfa_get_option_site_id', $main_site_id);
1341 switch_to_blog($get_option_site_id);
1342 $value = get_option($key);
1343 restore_current_blog();
1344 return $value;
1345 }
1346
1347 /**
1348 * Updates an option.
1349 *
1350 * @param String $key - option key
1351 * @param Mixed $value - option value
1352 *
1353 * @return Boolean
1354 */
1355 public function update_option($key, $value) {
1356 if (!is_multisite()) return update_option($key, $value);
1357
1358 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1359 $update_option_site_id = apply_filters('simba_tfa_update_option_site_id', $main_site_id);
1360
1361 switch_to_blog($update_option_site_id);
1362 $result = update_option($key, $value);
1363 restore_current_blog();
1364
1365 return $result;
1366 }
1367
1368 /**
1369 * Deletes an option.
1370 *
1371 * @param String $key - option key
1372 *
1373 * @return Boolean
1374 */
1375 public function delete_option($key) {
1376 if (!is_multisite()) return delete_option($key);
1377
1378 $main_site_id = function_exists('get_main_site_id') ? get_main_site_id() : 1;
1379 $delete_option_site_id = apply_filters('simba_tfa_delete_option_site_id', $main_site_id);
1380
1381 switch_to_blog($delete_option_site_id);
1382 $result = delete_option($key);
1383 restore_current_blog();
1384
1385 return $result;
1386 }
1387
1388 /**
1389 * Paint a list of checkboxes, one for each role
1390 *
1391 * @param String $prefix
1392 * @param Integer $default - default value (0 or 1)
1393 */
1394 public function list_user_roles_checkboxes($prefix = '', $default = 1) {
1395 if (is_multisite()) {
1396 // Not a real WP role; needs separate handling
1397 $id = '_super_admin';
1398 $name = __('Multisite Super Admin', 'two-factor-authentication');
1399 $setting = $this->get_option('tfa_'.$prefix.$id);
1400 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
1401
1402 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";
1403 }
1404
1405 global $wp_roles;
1406 if (!isset($wp_roles)) $wp_roles = new WP_Roles();
1407
1408 foreach ($wp_roles->role_names as $id => $name) {
1409 $setting = $this->get_option('tfa_'.$prefix.$id);
1410 $setting = ($setting === false) ? $default : ($setting ? 1 : 0);
1411
1412 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";
1413 }
1414
1415 }
1416
1417 public function tfa_list_xmlrpc_status_radios() {
1418
1419 $setting = $this->get_option('tfa_xmlrpc_on');
1420 $setting = $setting ? 1 : 0;
1421
1422 $types = array(
1423 '0' => __('Do not require 2FA over XMLRPC (best option if you must use XMLRPC and your client does not support 2FA)', 'two-factor-authentication'),
1424 '1' => __('Do require 2FA over XMLRPC (best option if you do not use XMLRPC or are unsure)', 'two-factor-authentication')
1425 );
1426
1427 foreach($types as $id => $name) {
1428 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";
1429 }
1430 }
1431
1432 protected function is_caller_active() {
1433
1434 if (!defined('XMLRPC_REQUEST') || !XMLRPC_REQUEST) return true;
1435
1436 $saved_data = $this->get_option('tfa_xmlrpc_on');
1437
1438 return $saved_data ? true : false;
1439
1440 }
1441
1442 /**
1443 * @param Array $params
1444 * @param Boolean $may_be_email
1445 *
1446 * @return WP_Error|Boolean|Integer - WP_Error or false means failure; true or 1 means success, but true means the TFA code was validated
1447 */
1448 public function authorise_user_from_login($params, $may_be_email = false) {
1449
1450 $params = apply_filters('simbatfa_auth_user_from_login_params', $params);
1451
1452 global $wpdb;
1453
1454 if (!$this->is_caller_active()) return 1;
1455
1456 $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']);
1457 $response = $wpdb->get_row($query);
1458
1459 if (!$response && $may_be_email && filter_var($params['log'], FILTER_VALIDATE_EMAIL)) {
1460 // Corner-case: login looks like an email, but is a username rather than email address
1461 $response = $wpdb->get_row($wpdb->prepare("SELECT ID, user_registered from ".$wpdb->users." WHERE user_login=%s", $params['log']));
1462 }
1463
1464 $user_id = is_object($response) ? $response->ID : false;
1465 $user_registered = is_object($response) ? $response->user_registered : false;
1466
1467 $user_code = isset($params['two_factor_code']) ? str_replace(' ', '', trim($params['two_factor_code'])) : '';
1468
1469 // This condition in theory should not be possible
1470 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')));
1471
1472 if (!$this->is_activated_for_user($user_id)) return 1;
1473
1474 if (!empty($params['trust_token']) && $this->user_trust_token_valid($user_id, $params['trust_token'])) {
1475 return 1;
1476 }
1477
1478 if (!$this->is_activated_by_user($user_id)) {
1479
1480 if (!$this->is_required_for_user($user_id)) return 1;
1481
1482 $enforce_require_after_check = true;
1483
1484 $require_enforce_after = $this->get_option('tfa_require_enforce_after');
1485
1486 // Don't enforce if the setting has never been saved
1487 if (is_string($require_enforce_after) && preg_match('#^(\d+)-(\d+)-(\d+)$#', $require_enforce_after, $enforce_matches)) {
1488
1489 // wp_date() is WP 5.3+, but performs translation into the site locale
1490 $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');
1491
1492 if (preg_match('#^(\d+)-(\d+)-(\d+)$#', $current_date, $current_date_matches)) {
1493 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])))) {
1494 // Enforcement not yet begun; skip
1495 $enforce_require_after_check = false;
1496 }
1497 }
1498
1499 }
1500
1501 $require_after = absint($this->get_option('tfa_requireafter')) * 86400;
1502
1503 $account_age = time() - strtotime($user_registered);
1504
1505 if ($account_age > $require_after && apply_filters('simbatfa_enforce_require_after_check', $enforce_require_after_check, $user_id, $require_after, $account_age)) {
1506
1507 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')));
1508 }
1509
1510 return 1;
1511 }
1512
1513 $tfa_creds_user_id = !empty($params['creds_user_id']) ? $params['creds_user_id'] : $user_id;
1514
1515 if ($tfa_creds_user_id != $user_id) {
1516
1517 // Authenticating using a different user's credentials (e.g. https://wordpress.org/plugins/use-administrator-password/)
1518 // In this case, we require that different user to have TFA active - so that this mechanism can't be used to avoid TFA
1519
1520 if (!$this->is_activated_for_user($tfa_creds_user_id) || !$this->is_activated_by_user($tfa_creds_user_id)) {
1521 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')));
1522 }
1523
1524 }
1525
1526 return $this->get_controller('totp')->check_code_for_user($tfa_creds_user_id, $user_code);
1527
1528 }
1529
1530 /**
1531 * Evaluate whether a trust token is valid for a user
1532 *
1533 * @param Integer $user_id - WP user ID
1534 * @param String $trust_token - trust token
1535 *
1536 * @return Boolean
1537 */
1538 protected function user_trust_token_valid($user_id, $trust_token) {
1539
1540 if (!is_string($trust_token) || strlen($trust_token) < 30) return false;
1541
1542 $trusted_devices = $this->user_get_trusted_devices($user_id);
1543
1544 $time_now = time();
1545
1546 foreach ($trusted_devices as $device) {
1547 if (empty($device['until']) || $device['until'] <= $time_now) continue;
1548 if (!empty($device['token']) && $device['token'] === $trust_token) {
1549 return true;
1550 }
1551 }
1552
1553 return false;
1554 }
1555
1556 /**
1557 * 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.
1558 *
1559 * @return String
1560 */
1561 protected function get_ajax_url() {
1562 $ajax_url = admin_url('admin-ajax.php');
1563 $parsed_url = parse_url($ajax_url);
1564 if (strtolower($parsed_url['host']) !== strtolower($_SERVER['HTTP_HOST']) && !empty($parsed_url['path'])) {
1565 // Mismatch - return the relative URL only
1566 $ajax_url = $parsed_url['path'];
1567 }
1568 return $ajax_url;
1569 }
1570
1571 /**
1572 * 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.
1573 */
1574 public function login_enqueue_scripts() {
1575 if (!$this->should_enqueue_login_scripts()) {
1576 return;
1577 }
1578
1579 if (isset($_GET['action']) && 'logout ' != $_GET['action'] && 'login' != $_GET['action']) return;
1580
1581 static $already_done = false;
1582 if ($already_done) return;
1583 $already_done = true;
1584
1585 // Prevent caching when in debug mode
1586 $script_ver = (defined('WP_DEBUG') && WP_DEBUG) ? time() : filemtime($this->includes_dir().'/tfa.js');
1587
1588 wp_enqueue_script('tfa-ajax-request', $this->includes_url().'/tfa.js', array('jquery'), $script_ver);
1589
1590 $trusted_for = $this->get_option('tfa_trusted_for');
1591 $trusted_for = (false === $trusted_for) ? 30 : (string) absint($trusted_for);
1592
1593 $localize = array(
1594 'ajaxurl' => $this->get_ajax_url(),
1595 'click_to_enter_otp' => __("Click to enter One Time Password", 'two-factor-authentication'),
1596 'enter_username_first' => __('You have to enter a username first.', 'two-factor-authentication'),
1597 'otp' => __('One Time Password (i.e. 2FA)', 'two-factor-authentication'),
1598 'otp_login_help' => __('(check your OTP app to get this password)', 'two-factor-authentication'),
1599 '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),
1600 'is_trusted' => __('(Trusted device - no OTP code required)', 'two-factor-authentication'),
1601 'nonce' => wp_create_nonce('simba_tfa_loginform_nonce'),
1602 'login_form_selectors' => '',
1603 'login_form_off_selectors' => '',
1604 'error' => __('An error has occurred. Site owners can check the JavaScript console for more details.', 'two-factor-authentication'),
1605 );
1606
1607 // Spinner exists since WC 3.8. Use the proper functions to avoid SSL warnings.
1608 if (file_exists(ABSPATH.'wp-admin/images/spinner-2x.gif')) {
1609 $localize['spinnerimg'] = admin_url('images/spinner-2x.gif');
1610 } elseif (file_exists(ABSPATH.WPINC.'/images/spinner-2x.gif')) {
1611 $localize['spinnerimg'] = includes_url('images/spinner-2x.gif');
1612 }
1613
1614 $localize = apply_filters('simba_tfa_login_enqueue_localize', $localize);
1615
1616 wp_localize_script('tfa-ajax-request', 'simba_tfasettings', $localize);
1617
1618 }
1619
1620 /**
1621 * Check whether TFA login scripts should be enqueued or not.
1622 *
1623 * @return boolean True if the TFA login script should be enqueued, otherwise false.
1624 */
1625 private function should_enqueue_login_scripts() {
1626 if (defined('TWO_FACTOR_DISABLE') && TWO_FACTOR_DISABLE) {
1627 return apply_filters('simbatfa_enqueue_login_scripts', false);
1628 }
1629
1630 global $wpdb;
1631 $sql = $wpdb->prepare('SELECT COUNT(user_id) FROM ' . $wpdb->usermeta . ' WHERE meta_key = %s AND meta_value = %d LIMIT 1', 'tfa_enable_tfa', 1);
1632 $count_user_id = $wpdb->get_var($sql);
1633
1634 if (is_null($count_user_id)) { // Error in query.
1635 return apply_filters('simbatfa_enqueue_login_scripts', true);
1636 } elseif ($count_user_id > 0) { // A user exists with TFA enabled.
1637 return apply_filters('simbatfa_enqueue_login_scripts', true);
1638 }
1639
1640 // No user exists with TFA enabled.
1641 return apply_filters('simbatfa_enqueue_login_scripts', false);
1642 }
1643
1644
1645 /**
1646 * Return or output view content
1647 *
1648 * @param String $path - path to template, usually relative to templates/ within the plugin directory
1649 * @param Array $extract_these - key/value pairs for substitution into the scope of the template
1650 * @param Boolean $return_instead_of_echo - what to do with the results
1651 *
1652 * @return String|Void
1653 */
1654 public function include_template($path, $extract_these = array(), $return_instead_of_echo = false) {
1655
1656 if ($return_instead_of_echo) ob_start();
1657
1658 $template_file = apply_filters('simatfa_template_file', $this->templates_dir().'/'.$path, $path, $extract_these, $return_instead_of_echo);
1659
1660 do_action('simbatfa_before_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1661
1662 if (!file_exists($template_file)) {
1663 error_log("TFA: template not found: $template_file (from $path)");
1664 echo __('Error:', 'two-factor-authentication').' '.__('Template path not found:', 'two-factor-authentication')." (".htmlspecialchars($path).")";
1665 } else {
1666 extract($extract_these);
1667 // The following are useful variables which can be used in the template.
1668 // They appear as unused, but may be used in the $template_file.
1669 $wpdb = $GLOBALS['wpdb'];// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wpdb might be used in the included template
1670 $simba_tfa = $this;// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1671 $totp_controller = $this->get_controller('totp');// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- $wp_optimize might be used in the included template
1672 include $template_file;
1673 }
1674
1675 do_action('simbatfa_after_template', $path, $return_instead_of_echo, $extract_these, $template_file);
1676
1677 if ($return_instead_of_echo) return ob_get_clean();
1678 }
1679
1680 /**
1681 * Make sure that self::$frontend is the instance of Simba_TFA_Frontend, and return it
1682 *
1683 * @return Simba_TFA_Frontend
1684 */
1685 public function load_frontend() {
1686 if (!class_exists('Simba_TFA_Frontend')) require_once($this->includes_dir().'/tfa_frontend.php');
1687 if (empty($this->frontend)) $this->frontend = new Simba_TFA_Frontend($this);
1688 return $this->frontend;
1689 }
1690
1691 // __return_empty_string() does not exist until WP 3.7
1692 public function shortcode_when_not_logged_in() {
1693 return '';
1694 }
1695
1696 /**
1697 * Set authentication slug.
1698 *
1699 * @param String $authentication_slug - Authentication slug. Verify that two-factor authentication should not be repeated for the same slug.
1700 */
1701 public function set_authentication_slug($authentication_slug) {
1702 $this->authentication_slug = $authentication_slug;
1703 }
1704
1705 /**
1706 * Unserialize data while maintaining compatibility across PHP versions due to different number of arguments required by PHP's "unserialize" function
1707 *
1708 * @param string $serialized_data Data to be unserialized, should be one that is already serialized
1709 * @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
1710 * @param integer $max_depth The maximum depth of structures permitted during unserialization, and is intended to prevent stack overflows
1711 *
1712 * @return mixed Unserialized data can be any of types (integer, float, boolean, string, array or object)
1713 */
1714 private static function unserialize($serialized_data, $allowed_classes = false, $max_depth = 0) {
1715 if (version_compare(PHP_VERSION, '7.0', '<')) {
1716 $result = unserialize($serialized_data);
1717 } else {
1718 $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
1719 }
1720 return $result;
1721 }
1722 }
1723