diff --git a/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.mysql.sql b/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.mysql.sql index 1528f6df0b..4130bdbb4b 100755 --- a/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.mysql.sql +++ b/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.mysql.sql @@ -30,3 +30,16 @@ SET t1.id_policy_module = ( SELECT t2.id_policy_module FROM tagente_modulo AS t2 WHERE t1.id_agente_modulo = t2.id_agente_modulo); + +/* 2014/12/10 */ +-- ---------------------------------------------------------------------- +-- Table `tuser_double_auth` +-- ---------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `tuser_double_auth` ( + `id` int(10) unsigned NOT NULL auto_increment, + `id_user` varchar(60) NOT NULL, + `secret` varchar(20) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`id_user`), + FOREIGN KEY (`id_user`) REFERENCES tusuario(`id_user`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.oracle.sql b/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.oracle.sql index 078ad51c72..6cd60011d4 100755 --- a/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.oracle.sql +++ b/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.oracle.sql @@ -18,4 +18,16 @@ ALTER TABLE tlayout_data ADD COLUMN fill_color varchar(200) DEFAULT ""; -- Table `ttag_module` -- --------------------------------------------------------------------- -ALTER TABLE tlayout_data ADD COLUMN id_policy_module NUMBER(10, 0) DEFAULT 0 NOT NULL; \ No newline at end of file +ALTER TABLE tlayout_data ADD COLUMN id_policy_module NUMBER(10, 0) DEFAULT 0 NOT NULL; + +/* 2014/12/10 */ +-- ---------------------------------------------------------------------- +-- Table `tuser_double_auth` +-- ---------------------------------------------------------------------- +CREATE TABLE tuser_double_auth ( + id NUMBER(10, 0) NOT NULL PRIMARY KEY, + id_user VARCHAR2(60) NOT NULL REFERENCES tusuario(id_user) ON DELETE CASCADE, + secret VARCHAR2(20) NOT NULL +); +CREATE SEQUENCE tuser_double_auth_s INCREMENT BY 1 START WITH 1; +CREATE OR REPLACE TRIGGER tuser_double_auth_inc BEFORE INSERT ON tuser_double_auth REFERENCING NEW AS NEW FOR EACH ROW BEGIN SELECT tuser_double_auth_s.nextval INTO :NEW.ID FROM dual; END tuser_double_auth_inc;; diff --git a/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.postgreSQL.sql b/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.postgreSQL.sql index d72ddf4e85..851a739872 100755 --- a/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.postgreSQL.sql +++ b/pandora_console/extras/pandoradb_migrate_5.1_to_6.0.postgreSQL.sql @@ -18,4 +18,14 @@ ALTER TABLE "tlayout_data" ADD COLUMN "fill_color" varchar(200) DEFAULT ""; -- Table `ttag_module` -- --------------------------------------------------------------------- -ALTER TABLE tlayout_data ADD COLUMN "id_policy_module" INTEGER NOT NULL DEFAULT 0; \ No newline at end of file +ALTER TABLE tlayout_data ADD COLUMN "id_policy_module" INTEGER NOT NULL DEFAULT 0; + +/* 2014/12/10 */ +-- ---------------------------------------------------------------------- +-- Table `tuser_double_auth` +-- ---------------------------------------------------------------------- +CREATE TABLE "tuser_double_auth" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "id_user" varchar(60) NOT NULL UNIQUE REFERENCES "tusuario"("id_user") ON DELETE CASCADE, + "secret" varchar(20) NOT NULL +); diff --git a/pandora_console/general/login_page.php b/pandora_console/general/login_page.php index 1e18e4e005..e126d9268d 100644 --- a/pandora_console/general/login_page.php +++ b/pandora_console/general/login_page.php @@ -35,6 +35,7 @@ switch ($login_screen) { $logo_title = __('Go to Pandora FMS Website'); break; case 'logout': + case 'double_auth': $logo_link = 'index.php'; $logo_title = __('Go to Login'); break; @@ -129,6 +130,22 @@ echo ' echo __('Your session is over. Please close your browser window to close this Pandora session.').'<br /><br />'; echo '</p>'; break; + case 'double_auth': + if (!empty ($page) && !empty ($sec)) { + foreach ($_POST as $key => $value) { + html_print_input_hidden ($key, $value); + } + } + echo '<div class="login_double_auth_code_text">'; + echo __('Authenticator code') . '<br>'; + echo '</div>'; + echo '<div class="login_double_auth_code">'; + html_print_input_text_extended ("auth_code", '', "auth_code", '', '', '' , false, '', 'class="login login_password"', false, true); + echo '</div>'; + echo '<div class="login_button">'; + html_print_submit_button(__("Check code") . ' >', "login_button", false, 'class="sub next_login"'); + echo '</div>'; + break; default: if (isset($error_info)) { echo '<h1 id="log_title">' . $error_info['title'] . '</h1>'; diff --git a/pandora_console/godmode/setup/setup_auth.php b/pandora_console/godmode/setup/setup_auth.php index 2c29c91633..1a077e1542 100644 --- a/pandora_console/godmode/setup/setup_auth.php +++ b/pandora_console/godmode/setup/setup_auth.php @@ -94,6 +94,15 @@ if (enterprise_installed()) { add_enterprise_auth_options($table, 12); } +// Enable double authentication +$row = array(); +$row[] = __('Double authentication') + . ui_print_help_tip(__("If this option is enabled, the users can use double authentication with their accounts"), true); +$row[] = __('Yes').' '.html_print_radio_button('double_auth_enabled', 1, '', $config['double_auth_enabled'], true) + .' ' + . __('No').' '.html_print_radio_button('double_auth_enabled', 0, '', $config['double_auth_enabled'], true); +$table->data[] = $row; + echo '<form id="form_setup" method="post">'; html_print_input_hidden ('update_config', 1); html_print_table ($table); diff --git a/pandora_console/include/ajax/double_auth.ajax.php b/pandora_console/include/ajax/double_auth.ajax.php new file mode 100644 index 0000000000..642ceb778a --- /dev/null +++ b/pandora_console/include/ajax/double_auth.ajax.php @@ -0,0 +1,522 @@ +<?php +// Pandora FMS - http://pandorafms.com +// ================================================== +// Copyright (c) 2005-2010 Artica Soluciones Tecnologicas +// Please see http://pandorafms.org for full contribution list + +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation for version 2. +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +global $config; + +// Login check +check_login (); + +// Security check +$id_user = (string) get_parameter('id_user'); +if ($id_user !== $config['id_user']) { + db_pandora_audit("ACL Violation", + "Trying to access Double Authentication"); + echo json_encode(-1); + return; +} + +// Load the class +require_once ($config['homedir'].'/include/auth/GAuth/Auth.php'); + +// Default lenght of the secret +$secret_lenght = 16; +// Default lenght of the code +$code_lenght = 6; + +// Generate a new secret for the user +$generate_double_auth_secret = (bool) get_parameter('generate_double_auth_secret'); +if ($generate_double_auth_secret) { + $gAuth = new \GAuth\Auth(); + $code = $gAuth->generateCode($secret_lenght); + + echo json_encode($code); + return; +} + +// Validate the provided secret with a code provided by the user. +// If the parameter 'save' is set to true, the secret will +// be stored into the database. +// The results can be true, false or 1 if the validation is true +// but the secret can't be stored into the database. +$validate_double_auth_code = (bool) get_parameter('validate_double_auth_code'); +if ($validate_double_auth_code) { + $result = false; + + $secret = (string) get_parameter('secret'); + + if (!empty($secret) && strlen($secret) === $secret_lenght) { + $code = (string) get_parameter('code'); + + if (!empty($code) && strlen($code) === $code_lenght) { + $save = (bool) get_parameter('save'); + + if (!empty($code)) { + $gAuth = new \GAuth\Auth($secret); + $result = $gAuth->validateCode($code); + } + + if ($result && $save) { + // Delete the actual value (if exists) + $where = array( + 'id_user' => $id_user + ); + db_process_sql_delete('tuser_double_auth', $where); + + // Insert the new value + $values = array( + 'id_user' => $id_user, + 'secret' => $secret + ); + $result = (bool) db_process_sql_insert('tuser_double_auth', $values); + + if (!$result) { + $result = 1; + } + } + } + } + + echo json_encode($result); + return; +} + +// Set the provided secret to the user +$save_double_auth_secret = (bool) get_parameter('save_double_auth_secret'); +if ($save_double_auth_secret) { + $result = false; + + $secret = (string) get_parameter('secret'); + + if (strlen($secret) === $secret_lenght) { + // Delete the actual value (if exists) + $where = array( + 'id_user' => $id_user + ); + db_process_sql_delete('tuser_double_auth', $where); + // Insert the new value + $values = array( + 'id_user' => $id_user, + 'secret' => $secret + ); + $result = (bool) db_process_sql_insert('tuser_double_auth', $values); + } + + echo json_encode($result); + return; +} + +// Disable the double auth for the user +$deactivate_double_auth = (bool) get_parameter('deactivate_double_auth'); +if ($deactivate_double_auth) { + $result = false; + + // Delete the actual value (if exists) + $where = array( + 'id_user' => $id_user + ); + $result = db_process_sql_delete('tuser_double_auth', $where); + + echo json_encode($result); + return; +} + +// Get the info page to the container dialog +$get_double_auth_data_page = (bool) get_parameter('get_double_auth_data_page'); +if ($get_double_auth_data_page) { + $secret = db_get_value('secret', 'tuser_double_auth', 'id_user', $id_user); + + if (empty($secret)) { + return; + } + + $html = ''; + $html .= "<div class=\"left_align\">"; + $html .= "<p>"; + $html .= __('This is the private code that you should use with your authenticator app') . ". "; + $html .= __('You could enter the code manually or use the QR code to add it automatically') . "."; + $html .= "</p>"; + $html .= "</div>"; + $html .= "<div class=\"center_align\">"; + $html .= __('Code') . ": <b>$secret</b>"; + $html .= "<br>"; + $html .= __('QR') . ": <br>"; + $html .= "<div id=\"qr-container\"></div>"; + $html .= "</div>"; + + ob_clean(); +?> +<script type="text/javascript"> + var secret = "<?php echo $secret; ?>"; + var userID = "<?php echo $config['id_user']; ?>"; + + // QR code with the secret to add it to the app + paint_qrcode("otpauth://totp/"+userID+"?secret="+secret, $("div#qr-container").get(0), 200, 200); + + $("div#qr-container").attr("title", "").find("canvas").remove(); + // Don't delete this timeout. It's necessary to perform the style change. + // Chrome min. milliseconds: 1. + // Firefox min. milliseconds: 9. + setTimeout(function() { + $("div#qr-container").find("img").attr("style", ""); + }, 10); +</script> +<?php + $html .= ob_get_clean(); + + echo $html; + return; +} + +// Get the info page to the container dialog +$get_double_auth_info_page = (bool) get_parameter('get_double_auth_info_page'); +if ($get_double_auth_info_page) { + $container_id = (string) get_parameter('containerID'); + + $html = ''; + $html .= "<div class=\"left_align\">"; + $html .= "<p>"; + $html .= __('You are about to activate the double authentication') . ". "; + $html .= __('With this option enabled, your account access will be more secure, + cause a code generated by other application will be required after the login') . ". "; + $html .= "</p>"; + $html .= "<p>"; + $html .= __('You will need to install the app from the following link before continue') . ". "; + $html .= "</p>"; + $html .= "</div>"; + $html .= "<br>"; + $html .= "<div class=\"center_align\">"; + $html .= html_print_button(__('Download the app'), 'google_authenticator_download', false, '', '', true); + $html .= "</div>"; + $html .= "<br>"; + $html .= "<div class=\"center_align\">"; + $html .= html_print_button(__('Continue'), 'continue_to_generate', false, '', '', true); + $html .= "</div>"; + + ob_clean(); +?> +<script type="text/javascript"> + // Open the download page on click + $("input[name=\"google_authenticator_download\"]").click(function (e) { + e.preventDefault(); + window.open("https://support.google.com/accounts/answer/1066447"); + }); + + // Change the container content with the generation page + $("input[name=\"continue_to_generate\"]").click(function (e) { + e.preventDefault(); + + if (!confirm("<?php echo __('Are you installed the app yet?'); ?>")) { + return false; + } + + var containerID = "<?php echo $container_id; ?>"; + + $("#"+containerID).html("<img src=\"<?php echo $config['homeurl']; ?>/images/spinner.gif\" />"); + + $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'html', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: "<?php echo $config['id_user']; ?>", + get_double_auth_generation_page: 1, + containerID: containerID + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + // isNaN = is not a number + if (isNaN(data)) { + $("#"+containerID).html(data); + } + // data is a number, convert it to integer to do the compare + else if (Number(data) === -1) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + else { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Error') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('There was an error loading the data') . '</div></b>'; ?>"); + } + }); + }); +</script> +<?php + $html .= ob_get_clean(); + + echo $html; + return; +} + +// Get the page that generates a secret for the user +$get_double_auth_generation_page = (bool) get_parameter('get_double_auth_generation_page'); +if ($get_double_auth_generation_page) { + $container_id = (string) get_parameter('containerID'); + + $gAuth = new \GAuth\Auth(); + $secret = $gAuth->generateCode($secret_lenght); + + $html = ''; + $html .= "<div class=\"center_align\">"; + $html .= "<p>"; + $html .= "<b>" . __('A private code has been generated') . "</b>."; + $html .= "</p>"; + $html .= "</div>"; + $html .= "<div class=\"left_align\">"; + $html .= "<p>"; + $html .= __('Before continue, you should create a new entry into the authenticator app') . ". "; + $html .= __('You could enter the code manually or use the QR code to add it automatically') . "."; + $html .= "</p>"; + $html .= "</div>"; + $html .= "<div class=\"center_align\">"; + $html .= __('Code') . ": <b>$secret</b>"; + $html .= "<br>"; + $html .= __('QR') . ": <br>"; + $html .= "<div id=\"qr-container\"></div>"; + $html .= "<br>"; + $html .= html_print_button(__('Refresh code'), 'continue_to_generate', false, '', '', true); + $html .= " "; + $html .= html_print_button(__('Continue'), 'continue_to_validate', false, '', '', true); + $html .= "</div>"; + + ob_clean(); +?> +<script type="text/javascript"> + var secret = "<?php echo $secret; ?>"; + var userID = "<?php echo $config['id_user']; ?>"; + + // QR code with the secret to add it to the app + paint_qrcode("otpauth://totp/"+userID+"?secret="+secret, $("div#qr-container").get(0), 200, 200); + + $("div#qr-container").attr("title", "").find("canvas").remove(); + // Don't delete this timeout. It's necessary to perform the style change. + // Chrome min. milliseconds: 1. + // Firefox min. milliseconds: 9. + setTimeout(function() { + $("div#qr-container").find("img").attr("style", ""); + }, 10); + + // Load the same page with another secret + $("input[name=\"continue_to_generate\"]").click(function(e) { + e.preventDefault(); + + var containerID = "<?php echo $container_id; ?>"; + + $("#"+containerID).html("<img src=\"<?php echo $config['homeurl']; ?>/images/spinner.gif\" />"); + + $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'html', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: userID, + get_double_auth_generation_page: 1, + containerID: containerID + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + // isNaN = is not a number + if (isNaN(data)) { + $("#"+containerID).html(data); + } + // data is a number, convert it to integer to do the compare + else if (Number(data) === -1) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + else { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Error') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('There was an error loading the data') . '</div></b>'; ?>"); + } + }); + }); + + // Load the validation page + $("input[name=\"continue_to_validate\"]").click(function(e) { + e.preventDefault(); + + if (!confirm("<?php echo __('Are you introduced the code in the authenticator app yet?'); ?>")) { + return false; + } + + var containerID = "<?php echo $container_id; ?>"; + + $("#"+containerID).html("<img src=\"<?php echo $config['homeurl']; ?>/images/spinner.gif\" />"); + + $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'html', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: "<?php echo $config['id_user']; ?>", + get_double_auth_validation_page: 1, + secret: secret, + containerID: containerID + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + // isNaN = is not a number + if (isNaN(data)) { + $("#"+containerID).html(data); + } + // data is a number, convert it to integer to do the compare + else if (Number(data) === -1) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + else { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Error') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('There was an error loading the data') . '</div></b>'; ?>"); + } + }); + }); +</script> +<?php + $html .= ob_get_clean(); + + echo $html; + return; +} + +// Get the validation page +$get_double_auth_validation_page = (bool) get_parameter('get_double_auth_validation_page'); +if ($get_double_auth_validation_page) { + $container_id = (string) get_parameter('containerID'); + $secret = (string) get_parameter('secret'); + + if (empty($secret) || strlen($secret) != $secret_lenght) { + echo json_encode(false); + return; + } + + $html = ''; + $html .= "<div class=\"left_align\">"; + $html .= "<p>"; + $html .= __('Introduce a code generated by the app') . ". "; + $html .= __('If the code is valid, the double authentication will be activated') . "."; + $html .= "</p>"; + $html .= "</div>"; + $html .= "<br>"; + $html .= "<div class=\"center_align\">"; + $html .= html_print_input_text('code', '', '', 50, $secret_lenght, true); + $html .= "<div id=\"code_input_message\" class=\"red\"></div>"; + $html .= "<br><br>"; + $html .= "<div id=\"button-container\">"; + $html .= html_print_button(__('Validate code'), 'continue_to_validate', false, '', '', true); + $html .= html_print_image ("images/spinner.gif", true); + $html .= "</div>"; + $html .= "</div>"; + + ob_clean(); +?> +<script type="text/javascript"> + $("div#button-container").find("img").hide(); + + // Start the error message hiden + $("div#code_input_message").hide(); + + var secret = "<?php echo $secret; ?>"; + + $("input#text-code").keypress(function() { + $(this).removeClass("red").css('border-color', '#cbcbcb'); + }); + + $("input[name=\"continue_to_validate\"]").click(function(e) { + e.preventDefault(); + + // Hide the error message + $("div#code_input_message").hide(); + + var containerID = "<?php echo $container_id; ?>"; + + $("input[name=\"continue_to_validate\"]").prop('enabled', false).hide(); + $("div#button-container").find("img").show(); + + $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'json', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: "<?php echo $config['id_user']; ?>", + validate_double_auth_code: 1, + save: 1, + secret: secret, + code: function () { + return $("input#text-code").val(); + }, + containerID: containerID + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + // Valid code + if (data === true) { + $("#"+containerID).html("<b><?php echo '<b><div class=\"green\">' . __('The code is valid, you can exit now') . '</div></b>'; ?></b>"); + } + // Invalid code + else if (data === false) { + $("input[name=\"continue_to_validate\"]").prop('enabled', true).show(); + $("div#button-container").find("img").hide(); + $("input#text-code").addClass("red").css('border-color', '#c00'); + + $("div#code_input_message").html("<?php echo __('Invalid code'); ?>").show(); + } + // Valid code but not saved + else if (data === 1) { + $("input[name=\"continue_to_validate\"]").prop('enabled', true).show(); + $("div#button-container").find("img").hide(); + $("input#text-code").addClass("red").css('border-color', '#c00'); + + $("div#code_input_message").html("<?php echo __('The code is valid, but it was an error saving the data'); ?>").show(); + } + // Authentication error + else if (data === -1) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + // Not expected results + else { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('Error') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $("#"+containerID).html("<?php echo '<b><div class=\"red\">' . __('There was an error loading the data') . '</div></b>'; ?>"); + } + }); + }); +</script> +<?php + $html .= ob_get_clean(); + + echo $html; + return; +} + +return; +?> \ No newline at end of file diff --git a/pandora_console/include/auth/GAuth/Auth.php b/pandora_console/include/auth/GAuth/Auth.php new file mode 100644 index 0000000000..2aa859238d --- /dev/null +++ b/pandora_console/include/auth/GAuth/Auth.php @@ -0,0 +1,356 @@ +<?php + +// The MIT License (MIT) + +// Copyright (c) 2013 Chris Cornutt + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace GAuth; + +/** + * A class for generating the codes compatible with the Google Authenticator + * clients. + * + * NOTE: A lot of the logic from this class has been borrowed from this class: + * http://www.idontplaydarts.com/wp-content/uploads/2011/07/ga.php_.txt + * + * @author Chris Cornutt <ccornutt@phpdeveloper.org> + * @package GAuth + * @license MIT + */ + +class Auth +{ + /** + * Internal lookup table + * @var array + */ + private $lookup = array(); + + /** + * Initialization key + * @var string + */ + private $initKey = null; + + /** + * Seconds between key refreshes + * @var integer + */ + private $refreshSeconds = 30; + + /** + * Length of codes to generate + * @var integer + */ + private $codeLength = 6; + + /** + * Range plus/minus for "window of opportunity" on allowed codes + * @var integer + */ + private $range = 2; + + /** + * Initialize the object and set up the lookup table + * Optionally the Initialization key + * + * @param string $initKey Initialization key + */ + public function __construct($initKey = null) + { + $this->buildLookup(); + + if ($initKey !== null) { + $this->setInitKey($initKey); + } + } + + /** + * Build the base32 lookup table + * + * @return null + */ + public function buildLookup() + { + $lookup = array_combine( + array_merge(range('A', 'Z'), range(2, 7)), + range(0, 31) + ); + $this->setLookup($lookup); + } + + /** + * Get the current "range" value + * @return integer Range value + */ + public function getRange() + { + return $this->range; + } + + /** + * Set the "range" value + * + * @param integer $range Range value + * @return \GAuth\Auth instance + */ + public function setRange($range) + { + if (!is_numeric($range)) { + throw new \InvalidArgumentException('Invalid window range'); + } + $this->range = $range; + return $this; + } + + /** + * Set the initialization key for the object + * + * @param string $key Initialization key + * @throws \InvalidArgumentException If hash is not valid base32 + * @return \GAuth\Auth instance + */ + public function setInitKey($key) + { + if (preg_match('/^['.implode('', array_keys($this->getLookup())).']+$/', $key) == false) { + throw new \InvalidArgumentException('Invalid base32 hash!'); + } + $this->initKey = $key; + return $this; + } + + /** + * Get the current Initialization key + * + * @return string Initialization key + */ + public function getInitKey() + { + return $this->initKey; + } + + /** + * Set the contents of the internal lookup table + * + * @param array $lookup Lookup data set + * @throws \InvalidArgumentException If lookup given is not an array + * @return \GAuth\Auth instance + */ + public function setLookup($lookup) + { + if (!is_array($lookup)) { + throw new \InvalidArgumentException('Lookup value must be an array'); + } + $this->lookup = $lookup; + return $this; + } + + /** + * Get the current lookup data set + * + * @return array Lookup data + */ + public function getLookup() + { + return $this->lookup; + } + + /** + * Get the number of seconds for code refresh currently set + * + * @return integer Refresh in seconds + */ + public function getRefresh() + { + return $this->refreshSeconds; + } + + /** + * Set the number of seconds to refresh codes + * + * @param integer $seconds Seconds to refresh + * @throws \InvalidArgumentException If seconds value is not numeric + * @return \GAuth\Auth instance + */ + public function setRefresh($seconds) + { + if (!is_numeric($seconds)) { + throw \InvalidArgumentException('Seconds must be numeric'); + } + $this->refreshSeconds = $seconds; + return $this; + } + + /** + * Get the current length for generated codes + * + * @return integer Code length + */ + public function getCodeLength() + { + return $this->codeLength; + } + + /** + * Set the length of the generated codes + * + * @param integer $length Code length + * @return \GAuth\Auth instance + */ + public function setCodeLength($length) + { + $this->codeLength = $length; + return $this; + } + + /** + * Validate the given code + * + * @param string $code Code entered by user + * @param string $initKey Initialization key + * @param string $timestamp Timestamp for calculation + * @param integer $range Seconds before/after to validate hash against + * @throws \InvalidArgumentException If incorrect code length + * @return boolean Pass/fail of validation + */ + public function validateCode($code, $initKey = null, $timestamp = null, $range = null) + { + if (strlen($code) !== $this->getCodeLength()) { + throw new \InvalidArgumentException('Incorrect code length'); + } + + $range = ($range == null) ? $this->getRange() : $range; + $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; + $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; + + $binary = $this->base32_decode($initKey); + + for ($time = ($timestamp - $range); $time <= ($timestamp + $range); $time++) { + if ($this->generateOneTime($binary, $time) == $code) { + return true; + } + } + return false; + } + + /** + * Generate a one-time code + * + * @param string $initKey Initialization key [optional] + * @param string $timestamp Timestamp for calculation [optional] + * @return string Geneerated code/hash + */ + public function generateOneTime($initKey = null, $timestamp = null) + { + $initKey = ($initKey == null) ? $this->getInitKey() : $initKey; + $timestamp = ($timestamp == null) ? $this->generateTimestamp() : $timestamp; + + $hash = hash_hmac ( + 'sha1', + pack('N*', 0) . pack('N*', $timestamp), + $initKey, + true + ); + + return str_pad($this->truncateHash($hash), $this->getCodeLength(), '0', STR_PAD_LEFT); + } + + /** + * Generate a code/hash + * Useful for making Initialization codes + * + * @param integer $length Length for the generated code + * @return string Generated code + */ + public function generateCode($length = 16) + { + $lookup = implode('', array_keys($this->getLookup())); + $code = ''; + + for ($i = 0; $i < $length; $i++) { + $code .= $lookup[mt_rand(0, strlen($lookup)-1)]; + } + + return $code; + } + + /** + * Geenrate the timestamp for the calculation + * + * @return integer Timestamp + */ + public function generateTimestamp() + { + return floor(microtime(true)/$this->getRefresh()); + } + + /** + * Truncate the given hash down to just what we need + * + * @param string $hash Hash to truncate + * @return string Truncated hash value + */ + public function truncateHash($hash) + { + $offset = ord($hash[19]) & 0xf; + + return ( + ((ord($hash[$offset+0]) & 0x7f) << 24 ) | + ((ord($hash[$offset+1]) & 0xff) << 16 ) | + ((ord($hash[$offset+2]) & 0xff) << 8 ) | + (ord($hash[$offset+3]) & 0xff) + ) % pow(10, $this->getCodeLength()); + } + + /** + * Base32 decoding function + * + * @param string base32 encoded hash + * @throws \InvalidArgumentException When hash is not valid + * @return string Binary value of hash + */ + public function base32_decode($hash) + { + $lookup = $this->getLookup(); + + if (preg_match('/^['.implode('', array_keys($lookup)).']+$/', $hash) == false) { + throw new \InvalidArgumentException('Invalid base32 hash!'); + } + + $hash = strtoupper($hash); + $buffer = 0; + $length = 0; + $binary = ''; + + for ($i = 0; $i < strlen($hash); $i++) { + $buffer = $buffer << 5; + $buffer += $lookup[$hash[$i]]; + $length += 5; + + if ($length >= 8) { + $length -= 8; + $binary .= chr(($buffer & (0xFF << $length)) >> $length); + } + } + + return $binary; + } +} \ No newline at end of file diff --git a/pandora_console/include/functions.php b/pandora_console/include/functions.php index bd2479ae65..46063ff02b 100644 --- a/pandora_console/include/functions.php +++ b/pandora_console/include/functions.php @@ -2236,4 +2236,55 @@ function print_audit_csv ($data) { } } +/** + * Validate the code given to surpass the 2 step authentication + * + * @param string User name + * @param string Code given by the authenticator app + * + * @return -1 if the parameters introduced are incorrect, + * there is a problem accessing the user secret or + * if an exception are launched. + * true if the code is valid. + * false if the code is invalid. + */ +function validate_double_auth_code ($user, $code) { + global $config; + require_once ($config['homedir'].'/include/auth/GAuth/Auth.php'); + $result = false; + + if (empty($user) || empty($code)) { + $result = -1; + } + else { + $secret = db_get_value('secret', 'tuser_double_auth', 'id_user', $user); + + if ($secret === false) { + $result = -1; + } + else if (!empty($secret)) { + try { + $gAuth = new \GAuth\Auth($secret); + $result = $gAuth->validateCode($code); + } catch (Exception $e) { + $result = -1; + } + } + } + + return $result; +} + +/** + * Get if the 2 step authentication is enabled for the user given + * + * @param string User name + * + * @return true if the user has the double auth enabled or false otherwise. + */ +function is_double_auth_enabled ($user) { + $result = (bool) db_get_value('id', 'tuser_double_auth', 'id_user', $user); + + return $result; +} ?> diff --git a/pandora_console/include/functions_config.php b/pandora_console/include/functions_config.php index 4d87e9b815..daf12e9367 100644 --- a/pandora_console/include/functions_config.php +++ b/pandora_console/include/functions_config.php @@ -321,6 +321,8 @@ function config_update_config () { $error_update[] = __('User'); if (!config_update_value ('rintegria_pass', get_parameter ('rintegria_pass'))) $error_update[] = __('Password'); + if (!config_update_value ('double_auth_enabled', get_parameter ('double_auth_enabled'))) + $error_update[] = __('Double authentication'); ///////////// break; case 'perf': diff --git a/pandora_console/include/styles/pandora.css b/pandora_console/include/styles/pandora.css index 3bff4851e5..596da5ec98 100755 --- a/pandora_console/include/styles/pandora.css +++ b/pandora_console/include/styles/pandora.css @@ -2820,3 +2820,17 @@ table#policy_modules td * { border-top-left-radius: 2px; border-top-right-radius: 2px; } + +#dialog-double_auth-container { + width: 100%; + text-align: center; + vertical-align: middle; +} + +.center_align { + text-align: center; +} + +.left_align { + text-align: left; +} \ No newline at end of file diff --git a/pandora_console/index.php b/pandora_console/index.php index d054197723..335b283dc9 100755 --- a/pandora_console/index.php +++ b/pandora_console/index.php @@ -167,176 +167,291 @@ if (strlen($search) > 0) { $searchPage = true; } -// Login process -if (! isset ($config['id_user']) && isset ($_GET["login"])) { - include_once('include/functions_db.php'); //Include it to use escape_string_sql function - - $config["auth_error"] = ""; //Set this to the error message from the authorization mechanism - $nick = get_parameter_post ("nick"); //This is the variable with the login - $pass = get_parameter_post ("pass"); //This is the variable with the password - $nick = db_escape_string_sql($nick); - $pass = db_escape_string_sql($pass); - - // process_user_login is a virtual function which should be defined in each auth file. - // It accepts username and password. The rest should be internal to the auth file. - // The auth file can set $config["auth_error"] to an informative error output or reference their internal error messages to it - // process_user_login should return false in case of errors or invalid login, the nickname if correct - $nick_in_db = process_user_login ($nick, $pass); - - $expired_pass = false; - - if (($nick_in_db != false) && ((!is_user_admin($nick) - || $config['enable_pass_policy_admin'])) - && (defined('PANDORA_ENTERPRISE')) - && ($config['enable_pass_policy'])) { - include_once(ENTERPRISE_DIR . "/include/auth/mysql.php"); +// Login process +if (! isset ($config['id_user'])) { + if (isset ($_GET["login"])) { + include_once('include/functions_db.php'); //Include it to use escape_string_sql function - $blocked = login_check_blocked($nick); - - if ($blocked) { - require_once ('general/login_page.php'); - db_pandora_audit("Password expired", "Password expired: ".$nick, $nick); - while (@ob_end_flush ()); - exit ("</html>"); + $config["auth_error"] = ""; //Set this to the error message from the authorization mechanism + $nick = get_parameter_post ("nick"); //This is the variable with the login + $pass = get_parameter_post ("pass"); //This is the variable with the password + $nick = db_escape_string_sql($nick); + $pass = db_escape_string_sql($pass); + + //Since now, only the $pass variable are needed + unset ($_GET['pass'], $_POST['pass'], $_REQUEST['pass']); + + // If the auth_code exists, we assume the user has come through the double auth page + if (isset ($_POST['auth_code'])) { + $double_auth_success = false; + + // The double authentication is activated and the user has surpassed the first step (the login). + // Now the authentication code provided will be checked. + if (isset ($_SESSION['prepared_login_da'])) { + if (isset ($_SESSION['prepared_login_da']['id_user']) + && isset ($_SESSION['prepared_login_da']['timestamp'])) { + + // The user has a maximum of 5 minutes to introduce the double auth code + $dauth_period = SECONDS_2MINUTES; + $now = time(); + $dauth_time = $_SESSION['prepared_login_da']['timestamp']; + + if ($now - $dauth_period < $dauth_time) { + // Nick + $nick = $_SESSION["prepared_login_da"]['id_user']; + // Code + $code = (string) get_parameter_post ("auth_code"); + + if (!empty($code)) { + $result = validate_double_auth_code($nick, $code); + + if ($result === true) { + // Double auth success + $double_auth_success = true; + } + else { + // Screen + $login_screen = 'double_auth'; + // Error message + $config["auth_error"] = __("Invalid code"); + + if (!isset($_SESSION['prepared_login_da']['attempts'])) + $_SESSION['prepared_login_da']['attempts'] = 0; + $_SESSION['prepared_login_da']['attempts']++; + } + } + else { + // Screen + $login_screen = 'double_auth'; + // Error message + $config["auth_error"] = __("The code shouldn't be empty"); + + if (!isset($_SESSION['prepared_login_da']['attempts'])) + $_SESSION['prepared_login_da']['attempts'] = 0; + $_SESSION['prepared_login_da']['attempts']++; + } + } + else { + // Expired login + unset ($_SESSION['prepared_login_da']); + + // Error message + $config["auth_error"] = __('Expired login'); + } + } + else { + // If the code doesn't exist, remove the prepared login + unset ($_SESSION['prepared_login_da']); + + // Error message + $config["auth_error"] = __('Login error'); + } + } + // If $_SESSION['prepared_login_da'] doesn't exist, the user have to do the login again + else { + // Error message + $config["auth_error"] = __('Login error'); + } + + // Remove the authenticator code + unset ($_POST['auth_code'], $code); + + if (!$double_auth_success) { + $login_failed = true; + require_once ('general/login_page.php'); + db_pandora_audit("Logon Failed", "Invalid double auth login: " + .$_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_ADDR']); + while (@ob_end_flush ()); + exit ("</html>"); + } } - //Checks if password has expired - $check_status = check_pass_status($nick, $pass); - - switch ($check_status) { - case PASSSWORD_POLICIES_FIRST_CHANGE: //first change - case PASSSWORD_POLICIES_EXPIRED: //pass expired - $expired_pass = true; - login_change_password($nick); - break; + if (isset ($double_auth_success) && $double_auth_success) { + // This values are true cause there are checked before complete the 2nd auth step + $nick_in_db = $_SESSION["prepared_login_da"]['id_user']; + $expired_pass = false; } - } - - if (($nick_in_db !== false) && $expired_pass) { - //login ok and password has expired - - require_once ('general/login_page.php'); - db_pandora_audit("Password expired", - "Password expired: " . $nick, $nick); - while (@ob_end_flush ()); - exit ("</html>"); - } - else if (($nick_in_db !== false) && (!$expired_pass)) { - //login ok and password has not expired - $process_login = true; - - echo "<script type='text/javascript'>var process_login_ok = 1;</script>"; - - unset ($_GET["sec2"]); - $_GET["sec"] = "general/logon_ok"; - $home_page =''; - if (isset($nick)) { - $user_info = users_get_user_by_id($nick); - $home_page = io_safe_output($user_info['section']); - $home_url = $user_info['data_section']; - if ($home_page != '') { - switch($home_page) { - case 'Event list': - $_GET["sec"] = "eventos"; - $_GET["sec2"] = "operation/events/events"; - break; - case 'Group view': - $_GET["sec"] = "estado"; - $_GET["sec2"] = "operation/agentes/group_view"; - break; - case 'Alert detail': - $_GET["sec"] = "estado"; - $_GET["sec2"] = "operation/agentes/alerts_status"; - break; - case 'Tactical view': - $_GET["sec"] = "estado"; - $_GET["sec2"] = "operation/agentes/tactical"; - break; - case 'Default': - $_GET["sec"] = "general/logon_ok"; - break; - case 'Dashboard': - $_GET["sec"] = "dashboard"; - $_GET["sec2"] = ENTERPRISE_DIR.'/dashboard/main_dashboard'; - break; - case 'Visual console': - $_GET["sec"] = "visualc"; - $_GET["sec2"] = "operation/visual_console/index"; - break; - case 'Other': - $home_url = io_safe_output($home_url); - parse_str ($home_url, $res); - $_GET["sec"] = $res["sec"]; - $_GET["sec2"] = $res["sec2"]; + else { + // process_user_login is a virtual function which should be defined in each auth file. + // It accepts username and password. The rest should be internal to the auth file. + // The auth file can set $config["auth_error"] to an informative error output or reference their internal error messages to it + // process_user_login should return false in case of errors or invalid login, the nickname if correct + $nick_in_db = process_user_login ($nick, $pass); + + $expired_pass = false; + + if (($nick_in_db != false) && ((!is_user_admin($nick) + || $config['enable_pass_policy_admin'])) + && (defined('PANDORA_ENTERPRISE')) + && ($config['enable_pass_policy'])) { + include_once(ENTERPRISE_DIR . "/include/auth/mysql.php"); + + $blocked = login_check_blocked($nick); + + if ($blocked) { + require_once ('general/login_page.php'); + db_pandora_audit("Password expired", "Password expired: ".$nick, $nick); + while (@ob_end_flush ()); + exit ("</html>"); + } + + //Checks if password has expired + $check_status = check_pass_status($nick, $pass); + + switch ($check_status) { + case PASSSWORD_POLICIES_FIRST_CHANGE: //first change + case PASSSWORD_POLICIES_EXPIRED: //pass expired + $expired_pass = true; + login_change_password($nick); break; } } - else { - $_GET["sec"] = "general/logon_ok"; - } - } - db_logon ($nick_in_db, $_SERVER['REMOTE_ADDR']); - $_SESSION['id_usuario'] = $nick_in_db; - $config['id_user'] = $nick_in_db; - //Remove everything that might have to do with people's passwords or logins - unset ($_GET['pass'], $pass, $_POST['pass'], $_REQUEST['pass'], $login_good); - - $user_language = get_user_language($config['id_user']); - - $l10n = NULL; - if (file_exists ('./include/languages/' . $user_language . '.mo')) { - $l10n = new gettext_reader (new CachedFileReader ('./include/languages/'.$user_language.'.mo')); - $l10n->load_tables(); - } - } - else { //login wrong - $blocked = false; - - if ((!is_user_admin($nick) || $config['enable_pass_policy_admin']) && defined('PANDORA_ENTERPRISE')) { - $blocked = login_check_blocked($nick); } - if (!$blocked) { - if (defined('PANDORA_ENTERPRISE')) { - login_check_failed($nick); //Checks failed attempts - } - $login_failed = true; + if (($nick_in_db !== false) && $expired_pass) { + //login ok and password has expired + require_once ('general/login_page.php'); - db_pandora_audit("Logon Failed", "Invalid login: ".$nick, $nick); + db_pandora_audit("Password expired", + "Password expired: " . $nick, $nick); while (@ob_end_flush ()); exit ("</html>"); } + else if (($nick_in_db !== false) && (!$expired_pass)) { + //login ok and password has not expired + + // Double auth check + if ((!isset ($double_auth_success) || !$double_auth_success) && is_double_auth_enabled($nick_in_db)) { + // Store this values in the session to know if the user login was correct + $_SESSION['prepared_login_da'] = array( + 'id_user' => $nick_in_db, + 'timestamp' => time(), + 'attempts' => 0 + ); + + // Load the page to introduce the double auth code + $login_screen = 'double_auth'; + require_once ('general/login_page.php'); + while (@ob_end_flush ()); + exit ("</html>"); + } + + //login ok and password has not expired + $process_login = true; + + echo "<script type='text/javascript'>var process_login_ok = 1;</script>"; + + unset ($_GET["sec2"]); + $_GET["sec"] = "general/logon_ok"; + $home_page =''; + if (isset($nick)) { + $user_info = users_get_user_by_id($nick); + $home_page = io_safe_output($user_info['section']); + $home_url = $user_info['data_section']; + if ($home_page != '') { + switch($home_page) { + case 'Event list': + $_GET["sec"] = "eventos"; + $_GET["sec2"] = "operation/events/events"; + break; + case 'Group view': + $_GET["sec"] = "estado"; + $_GET["sec2"] = "operation/agentes/group_view"; + break; + case 'Alert detail': + $_GET["sec"] = "estado"; + $_GET["sec2"] = "operation/agentes/alerts_status"; + break; + case 'Tactical view': + $_GET["sec"] = "estado"; + $_GET["sec2"] = "operation/agentes/tactical"; + break; + case 'Default': + $_GET["sec"] = "general/logon_ok"; + break; + case 'Dashboard': + $_GET["sec"] = "dashboard"; + $_GET["sec2"] = ENTERPRISE_DIR.'/dashboard/main_dashboard'; + break; + case 'Visual console': + $_GET["sec"] = "visualc"; + $_GET["sec2"] = "operation/visual_console/index"; + break; + case 'Other': + $home_url = io_safe_output($home_url); + parse_str ($home_url, $res); + $_GET["sec"] = $res["sec"]; + $_GET["sec2"] = $res["sec2"]; + break; + } + } + else { + $_GET["sec"] = "general/logon_ok"; + } + } + db_logon ($nick_in_db, $_SERVER['REMOTE_ADDR']); + $_SESSION['id_usuario'] = $nick_in_db; + $config['id_user'] = $nick_in_db; + //Remove everything that might have to do with people's passwords or logins + unset ($pass, $login_good); + + $user_language = get_user_language($config['id_user']); + + $l10n = NULL; + if (file_exists ('./include/languages/' . $user_language . '.mo')) { + $l10n = new gettext_reader (new CachedFileReader ('./include/languages/'.$user_language.'.mo')); + $l10n->load_tables(); + } + } + else { //login wrong + $blocked = false; + + if ((!is_user_admin($nick) || $config['enable_pass_policy_admin']) && defined('PANDORA_ENTERPRISE')) { + $blocked = login_check_blocked($nick); + } + + if (!$blocked) { + if (defined('PANDORA_ENTERPRISE')) { + login_check_failed($nick); //Checks failed attempts + } + $login_failed = true; + require_once ('general/login_page.php'); + db_pandora_audit("Logon Failed", "Invalid login: ".$nick, $nick); + while (@ob_end_flush ()); + exit ("</html>"); + } + else { + require_once ('general/login_page.php'); + db_pandora_audit("Logon Failed", "Invalid login: ".$nick, $nick); + while (@ob_end_flush ()); + exit ("</html>"); + } + } + } + // Hash login process + elseif (isset ($_GET["loginhash"])) { + $loginhash_data = get_parameter("loginhash_data", ""); + $loginhash_user = str_rot13(get_parameter("loginhash_user", "")); + + if ($config["loginhash_pwd"] != "" && $loginhash_data == md5($loginhash_user.$config["loginhash_pwd"])) { + db_logon ($loginhash_user, $_SERVER['REMOTE_ADDR']); + $_SESSION['id_usuario'] = $loginhash_user; + $config["id_user"] = $loginhash_user; + } else { require_once ('general/login_page.php'); - db_pandora_audit("Logon Failed", "Invalid login: ".$nick, $nick); + db_pandora_audit("Logon Failed (loginhash", "", "system"); while (@ob_end_flush ()); exit ("</html>"); } } -} -// Hash login process -elseif (! isset ($config['id_user']) && isset ($_GET["loginhash"])) { - $loginhash_data = get_parameter("loginhash_data", ""); - $loginhash_user = str_rot13(get_parameter("loginhash_user", "")); - - if ($config["loginhash_pwd"] != "" && $loginhash_data == md5($loginhash_user.$config["loginhash_pwd"])) { - db_logon ($loginhash_user, $_SERVER['REMOTE_ADDR']); - $_SESSION['id_usuario'] = $loginhash_user; - $config["id_user"] = $loginhash_user; - } + // There is no user connected else { require_once ('general/login_page.php'); - db_pandora_audit("Logon Failed (loginhash", "", "system"); while (@ob_end_flush ()); exit ("</html>"); } } -// There is no user connected -elseif (! isset ($config['id_user'])) { - require_once ('general/login_page.php'); - while (@ob_end_flush ()); - exit ("</html>"); -} // Log off if (isset ($_GET["bye"])) { diff --git a/pandora_console/mobile/include/user.class.php b/pandora_console/mobile/include/user.class.php index b79818e5c1..50da3360ea 100644 --- a/pandora_console/mobile/include/user.class.php +++ b/pandora_console/mobile/include/user.class.php @@ -19,22 +19,12 @@ class User { private $user; private $logged = false; private $errorLogin = false; + private $loginTime = false; private $logout_action = false; + private $needDoubleAuth = false; + private $errorDoubleAuth = false; - public function __construct($user = null, $password = null) { - $this->user = $user; - $this->errorLogin = false; - - if (process_user_login($this->user, $password)) { - $this->logged = true; - $this->hackInjectConfig(); - } - else { - $this->logged = false; - } - } - - public static function getInstance() { + public static function getInstance () { if (!(self::$instance instanceof self)) { //Check if in the session $system = System::getInstance(); @@ -51,22 +41,22 @@ class User { return self::$instance; } - public function hackInjectConfig() { - //hack to compatibility with pandora + public function saveLogin () { if ($this->logged) { - global $config; - $system = System::getInstance(); - - $config['id_user'] = $this->user; - - $system->setSessionBase('id_usuario', $this->user); $system->setSession('user', $this); + + if (!$this->needDoubleAuth) { + //hack to compatibility with pandora + global $config; + $config['id_user'] = $this->user; + $system->setSessionBase('id_usuario', $this->user); + } } } - public function isLogged() { + public function isLogged () { $system = System::getInstance(); $autologin = $system->getRequest('autologin', false); @@ -74,15 +64,13 @@ class User { $user = $system->getRequest('user', null); $password = $system->getRequest('password', null); - if ($this->checkLogin($user, $password)) { - $this->hackInjectConfig(); - } + $this->login($user, $password); } return $this->logged; } - - public function checkLogin($user = null, $password = null) { + + public function login ($user = null, $password = null) { $system = System::getInstance(); if (($user == null) && ($password == null)) { @@ -92,44 +80,126 @@ class User { } if (!empty($user) && !empty($password)) { - if (process_user_login($user, $password) !== false) { + $user_in_db = process_user_login($user, $password); + if ($user_in_db !== false) { $this->logged = true; - $this->user = $user; + $this->user = $user_in_db; + $this->loginTime = time(); $this->errorLogin = false; + + // The user login was successful, but the second step is not completed + if ($this->isDobleAuthRequired()) { + $this->needDoubleAuth = true; + } } else { $this->logged = false; + $this->loginTime = false; $this->errorLogin = true; + $this->needDoubleAuth = false; + $this->errorDoubleAuth = false; } } - if ($this->logged) { - $this->hackInjectConfig(); - - if (! check_acl($system->getConfig('id_user'), 0, "AR")) { - db_pandora_audit("ACL Violation", - "Trying to access Agent Data view"); - require ("../general/noaccess.php"); - return; - } - } + $this->saveLogin(); return $this->logged; } - - public function logout() { - $this->user = null; - $this->logged = false; - $this->errorLogin = false; - $this->logout_action = true; - - $system = System::getInstance(); - $system->setSession('user', null); + + public function getLoginTime () { + return $this->loginTime; } - public function showLogin() { + public function isWaitingDoubleAuth () { + return $this->needDoubleAuth; + } + + public function isDobleAuthRequired ($user = false) { + if (empty($user) && !empty($this->user)) + $user = $this->user; + + if (!empty($user)) + return (bool) db_get_value('id', 'tuser_double_auth', 'id_user', $user); + else + return false; + } + + public function validateDoubleAuthCode ($user = null, $code = null) { + + if (!$this->needDoubleAuth) { + return true; + } + + $system = System::getInstance(); + require_once ($system->getConfig('homedir').'/include/auth/GAuth/Auth.php'); + + $result = false; + + if (empty($user)) { + $user = $this->user; + } + if (empty($code)) { + $code = $system->getRequest('auth_code', null); + } + + if (!empty($user) && !empty($code)) { + $secret = db_get_value('secret', 'tuser_double_auth', 'id_user', $user); + + if ($secret === false) { + $result = false; + $this->errorDoubleAuth = array( + 'title_text' => __('Double authentication failed'), + 'content_text' => __('Secret code not found') .". " + .__('Please contact the administrator to reset your double authentication') + ); + } + else if (!empty($secret)) { + try { + $gAuth = new \GAuth\Auth($secret); + $result = $gAuth->validateCode($code); + + // Double auth success + if ($result) { + $this->needDoubleAuth = false; + $this->saveLogin(); + } + else { + $result = false; + $this->errorDoubleAuth = array( + 'title_text' => __('Double authentication failed'), + 'content_text' => __('Invalid code') + ); + } + } catch (Exception $e) { + $result = false; + $this->errorDoubleAuth = array( + 'title_text' => __('Double authentication failed'), + 'content_text' => __('There was an error checking the code') + ); + } + } + } + + return $result; + } + + public function logout () { + $this->user = null; + $this->logged = false; + $this->loginTime = false; + $this->errorLogin = false; + $this->logout_action = true; + $this->needDoubleAuth = false; + $this->errorDoubleAuth = false; + + $system = System::getInstance(); + $system->setSession('user', null); + $system->sessionDestroy(); + } + + public function showLoginPage () { global $pandora_version; $ui = Ui::getInstance(); @@ -191,16 +261,65 @@ class User { $this->errorLogin = false; $this->logout_action = false; } - - public function getIdUser() { + + public function showDoubleAuthPage () { + global $pandora_version; + + $ui = Ui::getInstance(); + + $ui->createPage(); + if (!empty($this->errorDoubleAuth)) { + $options['type'] = 'onStart'; + $options['title_text'] = $this->errorDoubleAuth['title_text']; + $options['content_text'] = $this->errorDoubleAuth['content_text'] . "<br>"; + $ui->addDialog($options); + } + $left_button = $ui->createHeaderButton( + array('icon' => 'back', + 'pos' => 'left', + 'text' => __('Logout'), + 'href' => 'index.php?action=logout')); + $ui->createHeader('', $left_button); + $ui->showFooter(false); + $ui->beginContent(); + $ui->contentAddHtml('<div style="text-align: center;" class="login_logo">' . + html_print_image ("mobile/images/pandora_mobile_console.png", + true, array ("alt" => "logo", "border" => 0)) . + '</div>'); + $ui->contentAddHtml('<div id="login_container">'); + $ui->beginForm(); + $ui->formAddHtml(html_print_input_hidden('action', 'double_auth', true)); + $options = array( + 'name' => 'auth_code', + 'value' => '', + 'placeholder' => __('Authenticator code'), + 'label' => __('Authenticator code') + ); + $ui->formAddInputPassword($options); + $options = array( + 'value' => __('Check code'), + 'icon' => 'arrow-r', + 'icon_pos' => 'right', + 'name' => 'auth_code_btn' + ); + $ui->formAddSubmitButton($options); + $ui->endForm(); + $ui->contentAddHtml('</div>'); + $ui->endContent(); + $ui->showPage(); + + $this->errorDoubleAuth = false; + } + + public function getIdUser () { return $this->user; //Oldies methods } - public function isInGroup($access = "AR", $id_group = 0, $name_group = false) { + public function isInGroup ($access = "AR", $id_group = 0, $name_group = false) { return (bool)check_acl($this->user, $id_group, $access); } - public function getIdGroups($access = "AR", $all = false) { + public function getIdGroups ($access = "AR", $all = false) { return array_keys(users_get_groups($this->user, $access, $all)); } } diff --git a/pandora_console/mobile/index.php b/pandora_console/mobile/index.php index a2fe7a5e3f..7a7f5c0414 100644 --- a/pandora_console/mobile/index.php +++ b/pandora_console/mobile/index.php @@ -43,13 +43,32 @@ $enterpriseHook = enterprise_include('mobile/operation/home.php'); $system = System::getInstance(); +require_once($system->getConfig('homedir').'/include/constants.php'); + $user = User::getInstance(); -$user->hackInjectConfig(); +$user->saveLogin(); $page = $system->getRequest('page', 'home'); $action = $system->getRequest('action'); -if (!$user->isLogged()) { - $action = 'login'; + +// The logout action has priority +if ($action != 'logout') { + if (!$user->isLogged()) { + $action = 'login'; + } + else if ($user->isWaitingDoubleAuth()) { + $dauth_period = SECONDS_2MINUTES; + $now = time(); + $dauth_time = $user->getLoginTime(); + + if ($now - $dauth_period < $dauth_time) { + $action = 'double_auth'; + } + // Expired login + else { + $action = 'logout'; + } + } } if ($action != "ajax") { @@ -107,11 +126,45 @@ switch ($action) { return; break; case 'login': - if (!$user->checkLogin()) { - $user->showLogin(); + if ($user->login() && $user->isLogged()) { + if ($user->isWaitingDoubleAuth()) { + if ($user->validateDoubleAuthCode()) { + $user_language = get_user_language ($system->getConfig('id_user')); + if (file_exists ('../include/languages/'.$user_language.'.mo')) { + $l10n = new gettext_reader (new CachedFileReader('../include/languages/'.$user_language.'.mo')); + $l10n->load_tables(); + } + if (class_exists("HomeEnterprise")) + $home = new HomeEnterprise(); + else + $home = new Home(); + $home->show(); + } + else { + $user->showDoubleAuthPage(); + } + } + else { + $user_language = get_user_language ($system->getConfig('id_user')); + if (file_exists ('../include/languages/'.$user_language.'.mo')) { + $l10n = new gettext_reader (new CachedFileReader('../include/languages/'.$user_language.'.mo')); + $l10n->load_tables(); + } + if (class_exists("HomeEnterprise")) + $home = new HomeEnterprise(); + else + $home = new Home(); + $home->show(); + } + } else { - if ($user->isLogged()) { + $user->showLoginPage(); + } + break; + case 'double_auth': + if ($user->isLogged()) { + if ($user->validateDoubleAuthCode()) { $user_language = get_user_language ($system->getConfig('id_user')); if (file_exists ('../include/languages/'.$user_language.'.mo')) { $l10n = new gettext_reader (new CachedFileReader('../include/languages/'.$user_language.'.mo')); @@ -124,13 +177,16 @@ switch ($action) { $home->show(); } else { - $user->showLoginFail(); + $user->showDoubleAuthPage(); } } + else { + $user->showLoginPage(); + } break; case 'logout': $user->logout(); - $user->showLogin(); + $user->showLoginPage(); break; default: if (class_exists("Enterprise")) { diff --git a/pandora_console/operation/users/user_edit.php b/pandora_console/operation/users/user_edit.php index ca2858d378..c3fa1f11b9 100644 --- a/pandora_console/operation/users/user_edit.php +++ b/pandora_console/operation/users/user_edit.php @@ -337,6 +337,23 @@ if (!$meta) { $table->data[] = $data; } +// Double auth +$double_auth_enabled = (bool) db_get_value('id', 'tuser_double_auth', 'id_user', $config['id_user']); +$data = array(); +$data[0] = __('Double authentication'); +$data[0] .= '<br>'; +$data[0] .= html_print_checkbox('double_auth', 1, $double_auth_enabled, true); +if ($double_auth_enabled) { + $data[0] .= ' '; + $data[0] .= html_print_button(__('Show information'), 'show_info', false, 'javascript:show_double_auth_info();', '', true); +} +// Dialog +$data[0] .= "<div id=\"dialog-double_auth\"><div id=\"dialog-double_auth-container\"></div></div>"; +$table->colspan[count($table->data)][0] = 3; +$table->rowclass[] = ''; +$table->rowstyle[] = 'font-weight: bold;'; +$table->data[] = $data; + $data = array(); $data[0] = __('Comments'); $table->colspan[count($table->data)][0] = 3; @@ -431,6 +448,17 @@ $(document).ready (function () { $("#text-block_size").removeAttr('disabled'); } } + + $("input#checkbox-double_auth").change(function (e) { + e.preventDefault(); + + if (this.checked) { + show_double_auth_activation(); + } + else { + show_double_auth_deactivation(); + } + }); show_data_section(); }); @@ -481,4 +509,209 @@ function show_data_section () { break; } } + +function show_double_auth_info () { + var userID = "<?php echo $config['id_user']; ?>"; + + var $loadingSpinner = $("<img src=\"<?php echo $config['homeurl']; ?>/images/spinner.gif\" />"); + var $dialogContainer = $("div#dialog-double_auth-container"); + + $dialogContainer.html($loadingSpinner); + + // Load the info page + var request = $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'html', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: userID, + get_double_auth_data_page: 1, + containerID: $dialogContainer.prop('id') + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + // isNaN = is not a number + if (isNaN(data)) { + $dialogContainer.html(data); + } + // data is a number, convert it to integer to do the compare + else if (Number(data) === -1) { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + else { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('Error') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('There was an error loading the data') . '</div></b>'; ?>"); + } + }); + + $("div#dialog-double_auth") + .append($dialogContainer) + .dialog({ + resizable: true, + draggable: true, + modal: true, + title: "<?php echo __('Double autentication information'); ?>", + overlay: { + opacity: 0.5, + background: "black" + }, + width: 400, + height: 375, + close: function(event, ui) { + // Abort the ajax request + if (typeof request != 'undefined') + request.abort(); + // Remove the contained html + $dialogContainer.empty(); + } + }) + .show(); + +} + +function show_double_auth_activation () { + var userID = "<?php echo $config['id_user']; ?>"; + + var $loadingSpinner = $("<img src=\"<?php echo $config['homeurl']; ?>/images/spinner.gif\" />"); + var $dialogContainer = $("div#dialog-double_auth-container"); + + $dialogContainer.html($loadingSpinner); + + // Load the info page + var request = $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'html', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: userID, + get_double_auth_info_page: 1, + containerID: $dialogContainer.prop('id') + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + // isNaN = is not a number + if (isNaN(data)) { + $dialogContainer.html(data); + } + // data is a number, convert it to integer to do the compare + else if (Number(data) === -1) { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + else { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('Error') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('There was an error loading the data') . '</div></b>'; ?>"); + } + }); + + $("div#dialog-double_auth").dialog({ + resizable: true, + draggable: true, + modal: true, + title: "<?php echo __('Double autentication activation'); ?>", + overlay: { + opacity: 0.5, + background: "black" + }, + width: 500, + height: 400, + close: function(event, ui) { + // Abort the ajax request + if (typeof request != 'undefined') + request.abort(); + // Remove the contained html + $dialogContainer.empty(); + + document.location.reload(); + } + }) + .show(); +} + +function show_double_auth_deactivation () { + var userID = "<?php echo $config['id_user']; ?>"; + + var $loadingSpinner = $("<img src=\"<?php echo $config['homeurl']; ?>/images/spinner.gif\" />"); + var $dialogContainer = $("div#dialog-double_auth-container"); + + var message = "<p><?php echo __('Are you sure?') . '<br>' . __('The double authentication will be deactivated'); ?></p>"; + var $button = $("<input type=\"button\" value=\"<?php echo __('Deactivate'); ?>\" />"); + + $dialogContainer + .empty() + .append(message) + .append($button); + + var request; + + $button.click(function(e) { + e.preventDefault(); + + $dialogContainer.html($loadingSpinner); + + // Deactivate the double auth + request = $.ajax({ + url: "<?php echo ui_get_full_url('ajax.php', false, false, false); ?>", + type: 'POST', + dataType: 'json', + data: { + page: 'include/ajax/double_auth.ajax', + id_user: userID, + deactivate_double_auth: 1 + }, + complete: function(xhr, textStatus) { + + }, + success: function(data, textStatus, xhr) { + if (data === -1) { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('Authentication error') . '</div></b>'; ?>"); + } + else if (data) { + $dialogContainer.html("<?php echo '<b><div class=\"green\">' . __('The double autentication was deactivated successfully') . '</div></b>'; ?>"); + } + else { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('There was an error deactivating the double autentication') . '</div></b>'; ?>"); + } + }, + error: function(xhr, textStatus, errorThrown) { + $dialogContainer.html("<?php echo '<b><div class=\"red\">' . __('There was an error deactivating the double autentication') . '</div></b>'; ?>"); + } + }); + }); + + + $("div#dialog-double_auth").dialog({ + resizable: true, + draggable: true, + modal: true, + title: "<?php echo __('Double autentication activation'); ?>", + overlay: { + opacity: 0.5, + background: "black" + }, + width: 300, + height: 150, + close: function(event, ui) { + // Abort the ajax request + if (typeof request != 'undefined') + request.abort(); + // Remove the contained html + $dialogContainer.empty(); + + document.location.reload(); + } + }) + .show(); +} </script> diff --git a/pandora_console/pandoradb.oracle.sql b/pandora_console/pandoradb.oracle.sql index 21d395fa01..9ddfa74875 100755 --- a/pandora_console/pandoradb.oracle.sql +++ b/pandora_console/pandoradb.oracle.sql @@ -1047,6 +1047,17 @@ CREATE TABLE tusuario_perfil ( CREATE SEQUENCE tusuario_perfil_s INCREMENT BY 1 START WITH 1; CREATE OR REPLACE TRIGGER tusuario_perfil_inc BEFORE INSERT ON tusuario_perfil REFERENCING NEW AS NEW FOR EACH ROW BEGIN SELECT tusuario_perfil_s.nextval INTO :NEW.ID_UP FROM dual; END tusuario_perfil_inc;; +-- ---------------------------------------------------------------------- +-- Table `tuser_double_auth` +-- ---------------------------------------------------------------------- +CREATE TABLE tuser_double_auth ( + id NUMBER(10, 0) NOT NULL PRIMARY KEY, + id_user VARCHAR2(60) NOT NULL REFERENCES tusuario(id_user) ON DELETE CASCADE, + secret VARCHAR2(20) NOT NULL +); +CREATE SEQUENCE tuser_double_auth_s INCREMENT BY 1 START WITH 1; +CREATE OR REPLACE TRIGGER tuser_double_auth_inc BEFORE INSERT ON tuser_double_auth REFERENCING NEW AS NEW FOR EACH ROW BEGIN SELECT tuser_double_auth_s.nextval INTO :NEW.ID FROM dual; END tuser_double_auth_inc;; + -- --------------------------------------------------------------------- -- Table "tnews" -- --------------------------------------------------------------------- diff --git a/pandora_console/pandoradb.postgreSQL.sql b/pandora_console/pandoradb.postgreSQL.sql index 384577eb55..2b51a3f349 100755 --- a/pandora_console/pandoradb.postgreSQL.sql +++ b/pandora_console/pandoradb.postgreSQL.sql @@ -926,6 +926,15 @@ CREATE TABLE "tusuario_perfil" ( "tags" text NOT NULL ); +-- ---------------------------------------------------------------------- +-- Table `tuser_double_auth` +-- ---------------------------------------------------------------------- +CREATE TABLE "tuser_double_auth" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "id_user" varchar(60) NOT NULL UNIQUE REFERENCES "tusuario"("id_user") ON DELETE CASCADE, + "secret" varchar(20) NOT NULL +); + -- ----------------------------------------------------- -- Table `tnews` -- ----------------------------------------------------- diff --git a/pandora_console/pandoradb.sql b/pandora_console/pandoradb.sql index df55a2711a..3b5ae82d42 100755 --- a/pandora_console/pandoradb.sql +++ b/pandora_console/pandoradb.sql @@ -999,6 +999,18 @@ CREATE TABLE IF NOT EXISTS `tusuario_perfil` ( PRIMARY KEY (`id_up`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- ---------------------------------------------------------------------- +-- Table `tuser_double_auth` +-- ---------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS `tuser_double_auth` ( + `id` int(10) unsigned NOT NULL auto_increment, + `id_user` varchar(60) NOT NULL, + `secret` varchar(20) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`id_user`), + FOREIGN KEY (`id_user`) REFERENCES tusuario(`id_user`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- ---------------------------------------------------------------------- -- Table `tnews` -- ---------------------------------------------------------------------- diff --git a/pandora_server/util/pandora_manage.pl b/pandora_server/util/pandora_manage.pl index faa45695aa..750bdfc9d2 100644 --- a/pandora_server/util/pandora_manage.pl +++ b/pandora_server/util/pandora_manage.pl @@ -154,6 +154,7 @@ sub help_screen{ help_screen_line('--add_profile_to_user', '<user_id> <profile_name> [<group_name>]', 'Add a profile in group to a user'); help_screen_line('--disable_eacl', '', 'Disable enterprise ACL system'); help_screen_line('--enable_eacl', '', 'Enable enterprise ACL system'); + help_screen_line('--disable_double_auth', '<user_name>', 'Disable the double authentication for the specified user'); print "\nEVENTS:\n\n" unless $param ne ''; help_screen_line('--create_event', "<event> <event_type> <group_name> [<agent_name> <module_name>\n\t <event_status> <severity> <template_name> <user_name> <comment> \n\t <source> <id_extra> <tags> <custom_data_json>]", 'Add event'); help_screen_line('--validate_event', "<agent_name> <module_name> <datetime_min> <datetime_max>\n\t <user_name> <criticity> <template_name>", 'Validate events'); @@ -3362,6 +3363,23 @@ sub cli_enable_eacl ($$) { exit; } +############################################################################### +# Disable double authentication +# Related option: --disable_double_auth +############################################################################### +sub cli_disable_double_auth () { + my $user_id = @ARGV[2]; + + print_log "[INFO] Disabling double authentication for the user '$user_id'\n\n"; + + $user_id = safe_input($user_id); + + # Delete the user secret + my $result = db_do ($dbh, 'DELETE FROM tuser_double_auth WHERE id_user = ?', $user_id); + + exit; +} + ############################################################################### # Enable user # Related option: --enable_user @@ -3691,21 +3709,25 @@ sub pandora_manage_main ($$$) { } elsif ($param eq '--disable_alerts') { param_check($ltotal, 0); - cli_disable_alerts ($conf, $dbh); - } + cli_disable_alerts ($conf, $dbh); + } elsif ($param eq '--enable_alerts') { param_check($ltotal, 0); - cli_enable_alerts ($conf, $dbh); - } + cli_enable_alerts ($conf, $dbh); + } elsif ($param eq '--disable_eacl') { param_check($ltotal, 0); - cli_disable_eacl ($conf, $dbh); - } + cli_disable_eacl ($conf, $dbh); + } elsif ($param eq '--enable_eacl') { param_check($ltotal, 0); - cli_enable_eacl ($conf, $dbh); - } - elsif ($param eq '--disable_group') { + cli_enable_eacl ($conf, $dbh); + } + elsif ($param eq '--disable_double_auth') { + param_check($ltotal, 1); + cli_disable_double_auth(); + } + elsif ($param eq '--disable_group') { param_check($ltotal, 1); cli_disable_group(); }