diff --git a/src/etc/inc/auth.inc b/src/etc/inc/auth.inc index 902637c4b..d4ca0609f 100644 --- a/src/etc/inc/auth.inc +++ b/src/etc/inc/auth.inc @@ -788,7 +788,12 @@ function auth_get_authserver_list() return $list; } -function authenticate_user($username, $password, $authcfg = NULL) +/** + * return authenticator object + * @param array|null $authcfg configuration + * @return Auth\Base type object + */ +function get_authenticator($authcfg = NULL) { if (empty($authcfg)) { $authName = 'Local Database'; @@ -804,8 +809,12 @@ function authenticate_user($username, $password, $authcfg = NULL) } $authFactory = new OPNsense\Auth\AuthenticationFactory; - $authenticator = $authFactory->get($authName); + return $authFactory->get($authName); +} +function authenticate_user($username, $password, $authcfg = NULL) +{ + $authenticator = get_authenticator($authcfg); if ($authenticator != null) { return $authenticator->authenticate($username, $password) ; } else { diff --git a/src/etc/inc/authgui.inc b/src/etc/inc/authgui.inc index 23545989d..8d88da175 100644 --- a/src/etc/inc/authgui.inc +++ b/src/etc/inc/authgui.inc @@ -195,7 +195,7 @@ function session_auth(&$Login_Error) } // Detect protocol change - if (!isset($_POST['login']) && !empty($_SESSION['Logged_In']) && $_SESSION['protocol'] != $config['system']['webgui']['protocol']) { + if (!isset($_POST['login']) && !empty($_SESSION['Username']) && $_SESSION['protocol'] != $config['system']['webgui']['protocol']) { session_write_close(); return false; } @@ -224,20 +224,34 @@ function session_auth(&$Login_Error) } // authenticate using config settings, or local if failed - if (authenticate_user($_POST['usernamefld'], $_POST['passwordfld'], $authcfg) || - ($authcfg_fallback !== false && authenticate_user($_POST['usernamefld'], $_POST['passwordfld'], $authcfg_fallback)) - ) { + $authenticator = get_authenticator($authcfg); + $is_authenticated = false; + if ($authenticator != null && $authenticator->authenticate($_POST['usernamefld'], $_POST['passwordfld'])) { + $is_authenticated = true; + } + if (!$is_authenticated && $authcfg_fallback !== false) { + $authenticator = get_authenticator($authcfg_fallback); + if ($authenticator != null && $authenticator->authenticate($_POST['usernamefld'], $_POST['passwordfld'])) { + $is_authenticated = true; + } + } + + if ($is_authenticated) { // Generate a new id to avoid session fixation session_regenerate_id(); - $_SESSION['Logged_In'] = "True"; $_SESSION['Username'] = $_POST['usernamefld']; $_SESSION['last_access'] = time(); $_SESSION['protocol'] = $config['system']['webgui']['protocol']; + if ($authenticator->shouldChangePassword($_SESSION['Username'])) { + $_SESSION['user_shouldChangePassword'] = true; + } if (!isset($config['system']['webgui']['quietlogin'])) { log_error(sprintf("Successful login for user '%s' from: %s", $_POST['usernamefld'], $_SERVER['REMOTE_ADDR'])); } if (!empty($_GET['url'])) { header(url_safe("Location: {$_GET['url']}")); + } elseif (!empty($_SESSION['user_shouldChangePassword'])) { + header("Location: system_usermanager_passwordmg.php"); } else { header(url_safe("Location: {$_SERVER['REQUEST_URI']}")); } @@ -249,7 +263,7 @@ function session_auth(&$Login_Error) } /* Show login page if they aren't logged in */ - if (empty($_SESSION['Logged_In'])) { + if (empty($_SESSION['Username'])) { session_write_close(); return false; } diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php b/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php index bedc7556c..0db9cb1b5 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php @@ -65,6 +65,28 @@ abstract class Base return $groups; } + /** + * check if password meets policy constraints, needs implementation if it applies. + * @param string $username username to check + * @param string $old_password current password + * @param string $new_password password to check + * @return array of unmet policy constraints + */ + public function checkPolicy($username, $old_password, $new_password) + { + return array(); + } + + /** + * check if the user should change his or hers password, needs implementation if it applies. + * @param string $username username to check + * @return boolean + */ + public function shouldChangePassword($username) + { + return false; + } + /** * user allowed in local group * @param string $username username to check diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/Local.php b/src/opnsense/mvc/app/library/OPNsense/Auth/Local.php index 36c29121f..c68d75b8e 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Auth/Local.php +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/Local.php @@ -64,6 +64,68 @@ class Local extends Base implements IAuthConnector return array(); } + /** + * check if password meets policy constraints + * @param string $username username to check + * @param string $old_password current password + * @param string $new_password password to check + * @return array of unmet policy constraints + */ + public function checkPolicy($username, $old_password, $new_password) + { + $result = array(); + $configObj = Config::getInstance()->object(); + if (!empty($configObj->system->webgui->enable_password_policy_constraints)) { + if (!empty($configObj->system->webgui->password_policy_length)) { + if (strlen($new_password) < $configObj->system->webgui->password_policy_length) { + $result[] = sprintf(gettext("Password must have at least %d characters"), + $configObj->system->webgui->password_policy_length); + } + } + if (!empty($configObj->system->webgui->password_policy_complexity)) { + $pwd_has_upper = preg_match_all('/[A-Z]/',$new_password, $o) > 0; + $pwd_has_lower = preg_match_all('/[a-z]/',$new_password, $o) > 0; + $pwd_has_number = preg_match_all('/[0-9]/',$new_password, $o) > 0; + $pwd_has_special = preg_match_all('/[!@#$%^&*()\-_=+{};:,<.>]/',$new_password, $o) > 0; + if ($old_password == $new_password) { + // equal password is not allowed + $result[] = gettext("Current password equals new password"); + } + if (($pwd_has_upper+$pwd_has_lower+$pwd_has_number+$pwd_has_special) < 3) { + // passwords should at least contain 3 of the 4 available character types + $result[] = gettext("Password should contain at least 3 of the 4 different character groups". + " (lowercase, uppercase, number, special)"); + } elseif (strpos($new_password, $username) !== false) { + $result[] = gettext("The username may not be a part of the password"); + } + } + } + return $result; + } + + /** + * check if the user should change his or hers password, calculated by the time difference of the last pwd change + * @param string $username username to check + */ + public function shouldChangePassword($username) + { + $configObj = Config::getInstance()->object(); + if (!empty($configObj->system->webgui->enable_password_policy_constraints)) { + if (!empty($configObj->system->webgui->password_policy_duration)) { + $duration = $configObj->system->webgui->password_policy_duration; + $userObject = $this->getUser($username); + if ($userObject != null) { + $now = microtime(true); + $pwdChangedAt = empty($userObject->pwd_changed_at) ? 0 : $userObject->pwd_changed_at; + if (abs($now - $pwdChangedAt)/60/60/24 >= $configObj->system->webgui->password_policy_duration) { + return true; + } + } + } + } + return false; + } + /** * authenticate user against local database (in config.xml) * @param string|SimpleXMLElement $username username (or xml object) to authenticate diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL.php b/src/opnsense/mvc/app/models/OPNsense/Core/ACL.php index 64ab3972b..165ecfaa8 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL.php +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL.php @@ -223,6 +223,9 @@ class ACL if ($url == '/index.php?logout') { // always allow logout, could use better structuring... return true; + } elseif (!empty($_SESSION['user_shouldChangePassword'])) { + // when a password change is enforced, lock all other endpoints + return $this->urlMatch($url, 'system_usermanager_passwordmg.php*'); } if (array_key_exists($username, $this->userDatabase)) { diff --git a/src/www/system_usermanager.php b/src/www/system_usermanager.php index 20dc32532..5d7e8290b 100644 --- a/src/www/system_usermanager.php +++ b/src/www/system_usermanager.php @@ -236,6 +236,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { if ($pconfig['passwordfld1'] != $pconfig['passwordfld2']) { $input_errors[] = gettext('The passwords do not match.'); + } elseif (empty($pconfig['gen_new_password'])) { + // check against local password policy + $authenticator = get_authenticator(); + $input_errors = array_merge( + $input_errors, $authenticator->checkPolicy($pconfig['usernamefld'], null, $pconfig['passwordfld1']) + ); } if (!empty($pconfig['passwordfld1']) && !empty($pconfig['gen_new_password'])) { diff --git a/src/www/system_usermanager_passwordmg.php b/src/www/system_usermanager_passwordmg.php index d976f6969..47d23c84f 100644 --- a/src/www/system_usermanager_passwordmg.php +++ b/src/www/system_usermanager_passwordmg.php @@ -47,6 +47,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (isset($_GET['savemsg'])) { $savemsg = htmlspecialchars(gettext($_GET['savemsg'])); + } elseif (!empty($_SESSION['user_shouldChangePassword'])) { + $savemsg = gettext("Your password has expired, please provide a new one"); } if ($userFound) { @@ -68,12 +70,23 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { if (!$userFound) { $input_errors[] = gettext("Sorry, you cannot change settings for a non-local user."); + } elseif (count($input_errors) == 0) { + $authenticator = get_authenticator(); + $input_errors = $authenticator->checkPolicy($username, $pconfig['passwordfld0'], $pconfig['passwordfld1']); } } if (count($input_errors) == 0) { $config['system']['user'][$userindex[$username]]['language'] = $pconfig['language']; - + // only update password change date if there is a policy constraint + if (!empty($config['system']['webgui']['enable_password_policy_constraints']) && + !empty($config['system']['webgui']['password_policy_length']) + ) { + $config['system']['user'][$userindex[$username]]['pwd_changed_at'] = microtime(true); + } + if (!empty($_SESSION['user_shouldChangePassword'])) { + unset($_SESSION['user_shouldChangePassword']); + } if ($pconfig['passwordfld1'] !== '' || $pconfig['passwordfld2'] !== '') { local_user_set_password($config['system']['user'][$userindex[$username]], $pconfig['passwordfld1']); local_user_set($config['system']['user'][$userindex[$username]]); diff --git a/src/www/system_usermanager_settings.php b/src/www/system_usermanager_settings.php index a96c11957..fb078185d 100644 --- a/src/www/system_usermanager_settings.php +++ b/src/www/system_usermanager_settings.php @@ -33,10 +33,17 @@ require_once("guiconfig.inc"); $save_and_test = false; if ($_SERVER['REQUEST_METHOD'] === 'GET') { $pconfig = array(); - $pconfig['session_timeout'] = $config['system']['webgui']['session_timeout']; - $pconfig['authmode'] = $config['system']['webgui']['authmode']; $pconfig['authmode_fallback'] = !empty($config['system']['webgui']['authmode_fallback']) ? $config['system']['webgui']['authmode_fallback'] : "Local Database"; - $pconfig['backend'] = $config['system']['webgui']['backend']; + foreach (array('session_timeout', 'authmode', 'password_policy_duration', + 'enable_password_policy_constraints', + 'password_policy_complexity', 'password_policy_length') as $fieldname) { + if (!empty($config['system']['webgui'][$fieldname])) { + $pconfig[$fieldname] = $config['system']['webgui'][$fieldname]; + } else { + $pconfig[$fieldname] = null; + } + } + } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { $pconfig = $_POST; $input_errors = array(); @@ -54,23 +61,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { } } - if (!empty($pconfig['session_timeout'])) { - $config['system']['webgui']['session_timeout'] = intval($pconfig['session_timeout']); - } elseif (isset($config['system']['webgui']['session_timeout'])) { - unset($config['system']['webgui']['session_timeout']); + foreach (array('session_timeout', 'authmode', 'authmode_fallback', 'password_policy_duration', + 'enable_password_policy_constraints', + 'password_policy_complexity', 'password_policy_length') as $fieldname) { + if (!empty($pconfig[$fieldname])) { + $config['system']['webgui'][$fieldname] = $pconfig[$fieldname]; + } elseif (isset($config['system']['webgui'][$fieldname])) { + unset($config['system']['webgui'][$fieldname]); + } } - if (!empty($pconfig['authmode'])) { - $config['system']['webgui']['authmode'] = $pconfig['authmode']; - } elseif (isset($config['system']['webgui']['authmode'])) { - unset($config['system']['webgui']['authmode']); - } - - if (!empty($pconfig['authmode_fallback'])) { - $config['system']['webgui']['authmode_fallback'] = $pconfig['authmode_fallback']; - } elseif (isset($config['system']['webgui']['authmode_fallback'])) { - unset($config['system']['webgui']['authmode_fallback']); - } write_config(); } @@ -81,7 +81,24 @@ include("head.inc"); ?>
+ +