implement password policies for local accounts. for https://github.com/opnsense/core/issues/2252

This change consists of two components:

1) enforcing the user to change his or her password every x days, when pwd_changed_at is not set or longer ago then specified only access to the password page is prohibited
2) enforce minimal length or complexity settings depending on selected choices
This commit is contained in:
Ad Schellevis 2018-03-11 18:24:23 +01:00
parent 8fb70ac4b1
commit dc74006c9a
8 changed files with 226 additions and 28 deletions

View File

@ -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 {

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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)) {

View File

@ -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'])) {

View File

@ -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]]);

View File

@ -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");
?>
<body>
<style>
.password_policy_constraints {
display:none;
}
</style>
<script>
$(document).ready(function() {
$("#enable_password_policy_constraints").change(function(){
if ($("#enable_password_policy_constraints").prop('checked')) {
$(".password_policy_constraints").show();
} else {
$(".password_policy_constraints").hide();
}
});
$("#enable_password_policy_constraints").change();
});
</script>
<?php
if ($save_and_test):?>
<script>
@ -146,7 +163,59 @@ endif;?>
</option>
</select>
</td>
</tr>
</tr>
<tr>
<td><a id="help_for_enable_password_policy_constraints" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext('Policy'); ?></td>
<td>
<input id="enable_password_policy_constraints" name="enable_password_policy_constraints" type="checkbox" <?= empty($pconfig['enable_password_policy_constraints']) ? '' : 'checked="checked"';?> />
<strong><?= gettext('Enable password policy constraints') ?></strong>
<output class="hidden" for="help_for_enable_password_policy_constraints">
<?= gettext("Harden security on local accounts, for methods other then local these will usually be configured on the " .
"respective provider (e.g. ldap/radius/..). ");?>
</output>
</td>
</tr>
<tr class="password_policy_constraints">
<td><a id="help_for_password_policy_duration" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext('Duration'); ?></td>
<td>
<select id="password_policy_duration" name="password_policy_duration" class="selectpicker" data-style="btn-default">
<option <?=empty($pconfig['password_policy_duration']) ? "selected=\"selected\"" : "";?> value="0"><?=gettext("Disable");?></option>
<option <?=$pconfig['password_policy_duration'] == '30' ? "selected=\"selected\"" : "";?> value="30"><?=sprintf(gettext("%d days"), "30");?></option>
<option <?=$pconfig['password_policy_duration'] == '90' ? "selected=\"selected\"" : "";?> value="90"><?=sprintf(gettext("%d days"), "90");?></option>
<option <?=$pconfig['password_policy_duration'] == '180' ? "selected=\"selected\"" : "";?> value="180"><?=sprintf(gettext("%d days"), "180");?></option>
<option <?=$pconfig['password_policy_duration'] == '360' ? "selected=\"selected\"" : "";?> value="360"><?=sprintf(gettext("%d days"), "360");?></option>
</select>
<output class="hidden" for="help_for_password_policy_duration">
<?= gettext("Password duration settings, the interval in days in which passwords stay valid. ".
"When reached, the user will be forced to change his or her password before continuing.");?>
</output>
</td>
</tr>
<tr class="password_policy_constraints">
<td><a id="help_for_password_policy_length" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext('Length'); ?></td>
<td>
<select id="password_policy_length" name="password_policy_length" class="selectpicker" data-style="btn-default">
<option <?=empty($pconfig['password_policy_length']) || $pconfig['password_policy_length'] == '8' ? "selected=\"selected\"" : "";?> value="8">8</option>
<option <?=$pconfig['password_policy_length'] == '10' ? "selected=\"selected\"" : "";?> value="10">10</option>
<option <?=$pconfig['password_policy_length'] == '12' ? "selected=\"selected\"" : "";?> value="12">12</option>
<option <?=$pconfig['password_policy_length'] == '14' ? "selected=\"selected\"" : "";?> value="14">14</option>
<option <?=$pconfig['password_policy_length'] == '16' ? "selected=\"selected\"" : "";?> value="16">16</option>
</select>
<output class="hidden" for="help_for_password_policy_length">
<?= gettext("Sets the minimum length for a password");?>
</output>
</td>
</tr>
<tr class="password_policy_constraints">
<td><a id="help_for_password_policy_complexity" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext('Complexity'); ?></td>
<td>
<input id="password_policy_complexity" name="password_policy_complexity" type="checkbox" <?= empty($pconfig['password_policy_complexity']) ? '' : 'checked="checked"';?> />
<strong><?= gettext('Enable complexity requirements') ?></strong>
<output class="hidden" for="help_for_password_policy_complexity">
<?= gettext("Require passwords to meet complexity rules");?>
</output>
</td>
</tr>
<tr>
<td></td>
<td>