Commit 130c6634 authored by Patrick Müller's avatar Patrick Müller

feat: Restrict login for X seconds after failed login attempt

parent a23fe37e
......@@ -33,6 +33,8 @@
<field type="VARCHAR(5) NULL">shortcuts</field>
<field type="TEXT NULL">authenticator</field>
<field type="VARCHAR(50) NOT NULL">uuid</field>
<field type="DATETIME NULL DEFAULT NULL">lastLoginAttempt</field>
<field type="SMALLINT UNSIGNED NOT NULL DEFAULT '0'">failedLogins</field>
<primary>id</primary>
<index>username,password</index>
......
......@@ -3,5 +3,11 @@
<event on="onAdminLoadFooter" fire="\QUI\EventHandler::onAdminLoadFooter"/>
<event on="onUserChangePassword" fire="\QUI\EventHandler::onUserChangePassword"/>
<event on="onPackageSetup" fire="\QUI\Users\Auth\Handler::onPackageSetup"/>
<event on="onPackageUpdate" fire="\QUI\EventHandler::onPackageUpdate" />
<event on="onPackageUpdate" fire="\QUI\EventHandler::onPackageUpdate"/>
<event on="onUserLoginError" fire="\QUI\EventHandler::onUserLoginError"/>
<event on="onUserLoginStart" fire="\QUI\EventHandler::onUserLoginStart"/>
<event on="onUserLogin" fire="\QUI\EventHandler::onUserLogin"/>
<event on="onUserAuthenticatorLoginStart" fire="\QUI\EventHandler::onUserAuthenticatorLoginStart"/>
</events>
\ No newline at end of file
......@@ -102,4 +102,118 @@ class EventHandler
}
}
}
/**
* quiqqer/quiqqer: onUserLoginError
*
* Increase User failedLogins counter
*
* @param int $userId - ID of the QUIQQER user that tries to log in
* @param QUI\Users\Exception $Exception
* @return void
*/
public static function onUserLoginError($userId, QUI\Users\Exception $Exception)
{
switch ($Exception->getAttribute('reason')) {
case QUI\Users\Manager::AUTH_ERROR_AUTH_ERROR:
break;
default:
return;
}
try {
$User = QUI::getUsers()->get($userId);
$failedLogins = $User->getAttribute('failedLogins');
if (empty($failedLogins)) {
$failedLogins = 0;
}
$User->setAttributes(array(
'failedLogins' => ++$failedLogins,
'lastLoginAttempt' => date('Y-m-d H:i:s')
));
$User->save(QUI::getUsers()->getSystemUser());
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
}
}
/**
* quiqqer/quiqer: userAuthenticatorLoginStart
*
* @param int|false $userId
* @param string $authenticator
* @return void
*
* @throws QUI\Users\Exception
*/
public static function onUserAuthenticatorLoginStart($userId, $authenticator)
{
self::onUserLoginStart($userId);
}
/**
* quiqqer/quiqqer: onUserLoginStart
*
* @param int|false $userId
* @return void
*
* @throws QUI\Users\Exception
*/
public static function onUserLoginStart($userId)
{
if (!$userId) {
return;
}
try {
$User = QUI::getUsers()->get((int)$userId);
} catch (\Exception $Exception) {
// do nothing if user cannot be found
return;
}
$failedLogins = (int)$User->getAttribute('failedLogins');
$lastLoginAttempt = $User->getAttribute('lastLoginAttempt');
if (!$failedLogins || !$lastLoginAttempt) {
return;
}
$NextLoginAllowed = new \DateTime($lastLoginAttempt . ' +' . $failedLogins . ' second');
$Now = new \DateTime();
if ($Now < $NextLoginAllowed) {
throw new QUI\Users\Exception(
array(
'quiqqer/system',
'exception.login.fail.login_locked'
),
404
);
}
}
/**
* quiqqer/quiqqer: onUserLogin
*
* @param Users\User $User
* @return void
*/
public static function onUserLogin(QUI\Users\User $User)
{
try {
$User->setAttributes(array(
'failedLogins' => 0,
'lastLoginAttempt' => false
));
$User->save(QUI::getUsers()->getSystemUser());
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
}
}
}
......@@ -26,12 +26,11 @@ use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
*/
class Manager
{
const AUTH_ERROR_PRIMARY_AUTH_ERROR = 'auth_error_primary_auth_error';
const AUTH_ERROR_SECONDARY_AUTH_ERROR = 'auth_error_secondary_auth_error';
const AUTH_ERROR_USER_NOT_FOUND = 'auth_error_user_not_found';
const AUTH_ERROR_USER_NOT_ACTIVE = 'auth_error_user_not_active';
const AUTH_ERROR_LOGIN_EXPIRED = 'auth_error_login_expired';
const AUTH_ERROR_NO_ACTIVE_GROUP = 'auth_error_no_active_group';
const AUTH_ERROR_AUTH_ERROR = 'AUTH_ERROR_AUTH_ERROR';
const AUTH_ERROR_USER_NOT_FOUND = 'auth_error_user_not_found';
const AUTH_ERROR_USER_NOT_ACTIVE = 'auth_error_user_not_active';
const AUTH_ERROR_LOGIN_EXPIRED = 'auth_error_login_expired';
const AUTH_ERROR_NO_ACTIVE_GROUP = 'auth_error_no_active_group';
/**
* @var QUI\Projects\Project (active internal project)
......@@ -663,6 +662,20 @@ class Manager
$username = $params['username'];
}
// try to get user id
$userId = false;
if (!empty($username)) {
try {
$User = self::getUserByName($username);
$userId = $User->getId();
} catch (\Exception $Exception) {
// nothing
}
}
QUI::getEvents()->fireEvent('userAuthenticatorLoginStart', array($userId, $authenticator));
if ($authenticator instanceof AuthenticatorInterface) {
$Authenticator = $authenticator;
} else {
......@@ -682,6 +695,10 @@ class Manager
try {
$Authenticator->auth($params);
} catch (QUI\Users\Exception $Exception) {
$Exception->setAttribute('reason', self::AUTH_ERROR_AUTH_ERROR);
QUI::getEvents()->fireEvent('userLoginError', array($userId, $Exception));
throw $Exception;
} catch (\Exception $Exception) {
QUI\System\Log::writeException($Exception);
......@@ -748,36 +765,32 @@ class Manager
);
}
// try to get userId by authData
if (!empty($authData['username'])) {
try {
$User = self::getUserByName($authData['username']);
$userId = $User->getId();
} catch (\Exception $Exception) {
// nothing
}
}
$Events->fireEvent('userLoginStart', array($userId));
// global authenticators
if (QUI::getSession()->get('auth-globals') !== 1) {
$authenticators = QUI\Users\Auth\Handler::getInstance()->getGlobalAuthenticators();
/* @var $Authenticator QUI\Users\AbstractAuthenticator */
foreach ($authenticators as $authenticator) {
try {
$this->authenticate($authenticator, $authData);
} catch (\Exception $Exception) {
$Exception = new QUI\Users\Exception(
array('quiqqer/system', 'exception.login.fail.authenticator_error'),
404
);
$Exception->setAttribute('reason', self::AUTH_ERROR_PRIMARY_AUTH_ERROR);
$Events->fireEvent('userLoginError', array($userId, $Exception));
throw $Exception;
}
$this->authenticate($authenticator, $authData);
}
QUI::getSession()->set('auth-globals', 1);
}
$userId = QUI::getSession()->get('uid');
$Events->fireEvent('userLoginStart', array($userId));
$User = $this->get($userId);
$User = $this->get($userId);
if (QUI::getUsers()->isNobodyUser($User)) {
$Exception = new QUI\Users\Exception(
......@@ -852,18 +865,8 @@ class Manager
// user authenticators
$authenticator = $User->getAuthenticators();
try {
foreach ($authenticator as $Authenticator) {
$this->authenticate($Authenticator, $authData);
}
} catch (\Exception $Exception) {
$Events->fireEvent('userLoginError', array($userId, $Exception));
if (method_exists($Exception, 'setAttribute')) {
$Exception->setAttribute('reason', self::AUTH_ERROR_SECONDARY_AUTH_ERROR);
}
throw $Exception;
foreach ($authenticator as $Authenticator) {
$this->authenticate($Authenticator, $authData);
}
// is one group active?
......
......@@ -581,7 +581,7 @@ class User implements QUI\Interfaces\Users\User
$lastname = $this->getAttribute('lastname');
if ($firstname && $lastname) {
return $firstname.' '.$lastname;
return $firstname . ' ' . $lastname;
}
return $this->getUsername();
......@@ -781,7 +781,7 @@ class User implements QUI\Interfaces\Users\User
}
}
$this->groups = ','.implode($aTmp, ',').',';
$this->groups = ',' . implode($aTmp, ',') . ',';
return;
}
......@@ -803,7 +803,7 @@ class User implements QUI\Interfaces\Users\User
}
}
$this->groups = ','.implode($aTmp, ',').',';
$this->groups = ',' . implode($aTmp, ',') . ',';
return;
}
......@@ -812,7 +812,7 @@ class User implements QUI\Interfaces\Users\User
if (is_string($groups)) {
try {
$this->Group[] = $Groups->get($groups);
$this->groups = ','.$groups.',';
$this->groups = ',' . $groups . ',';
} catch (QUI\Exception $Exception) {
}
}
......@@ -1493,7 +1493,7 @@ class User implements QUI\Interfaces\Users\User
Manager::table(),
array(
'username' => $this->getUsername(),
'usergroup' => ','.implode(',', $this->getGroups(false)).',',
'usergroup' => ',' . implode(',', $this->getGroups(false)) . ',',
'firstname' => $this->getAttribute('firstname'),
'lastname' => $this->getAttribute('lastname'),
'usertitle' => $this->getAttribute('usertitle'),
......@@ -1510,7 +1510,9 @@ class User implements QUI\Interfaces\Users\User
'company' => $this->isCompany() ? 1 : 0,
'toolbar' => $toolbar,
'assigned_toolbar' => $assignedToolbars,
'authenticator' => json_encode($this->authenticator)
'authenticator' => json_encode($this->authenticator),
'lastLoginAttempt' => $this->getAttribute('lastLoginAttempt') ?: null,
'failedLogins' => $this->getAttribute('failedLogins') ?: 0
),
array('id' => $this->getId())
);
......@@ -1698,7 +1700,7 @@ class User implements QUI\Interfaces\Users\User
foreach ($list as $entry) {
$plugin = $entry['name'];
$userXml = OPT_DIR.$plugin.'/user.xml';
$userXml = OPT_DIR . $plugin . '/user.xml';
if (!file_exists($userXml)) {
continue;
......@@ -1725,7 +1727,7 @@ class User implements QUI\Interfaces\Users\User
*/
protected function readAttributesFromUserXML($file)
{
$cache = 'user/plugin-xml-attributes-'.md5($file);
$cache = 'user/plugin-xml-attributes-' . md5($file);
try {
return QUI\Cache\Manager::get($cache);
......
......@@ -6901,6 +6901,10 @@ Folgende Zeichen sind erlaubt: 0-9 a-z A-Z _ -</p>]]></de>
<de><![CDATA[Login fehlgeschlagen. Benutzer ist nicht aktiv.]]></de>
<en><![CDATA[Login failed. User is not active.]]></en>
</locale>
<locale name="exception.login.fail.login_locked">
<de><![CDATA[Login fehlgeschlagen. Der Login für diesen Benutzer ist z.Z. gesperrt. Bitte versuchen Sie es später noch einmal.]]></de>
<en><![CDATA[Login failed. Login for this user is currently locked. Please try again later.]]></en>
</locale>
<locale name="message.site.save.success">
<de><![CDATA[Die Seite [id] [title] wurde erfolgreich gespeichert.]]></de>
<en><![CDATA[The page [id] [title] are successfuly saved.]]]></en>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment