Skip to content

Commit

Permalink
config.yml: added backend_login_policy (#5197)
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Jul 16, 2022
1 parent caffa38 commit b05f1a1
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 15 deletions.
6 changes: 6 additions & 0 deletions redaxo/src/core/default.config.yml
Expand Up @@ -10,6 +10,12 @@ fileperm: '0664'
dirperm: '0775'
session_duration: 7200
session_keep_alive: 21600
backend_login_policy:
login_tries_1: 3
relogin_delay_1: 5
login_tries_2: 50
relogin_delay_2: 3600
enable_stay_logged_in: true
# using separate cookie domains for frontend and backend is more secure,
# but be warned that some features like detecting a backend user in the frontend
# will no longer work.
Expand Down
41 changes: 31 additions & 10 deletions redaxo/src/core/lib/login/backend_login.php
Expand Up @@ -9,17 +9,16 @@
class rex_backend_login extends rex_login
{
public const SYSTEM_ID = 'backend_login';
public const LOGIN_TRIES_1 = 3;
public const RELOGIN_DELAY_1 = 5; // relogin delay after LOGIN_TRIES_1 tries
public const LOGIN_TRIES_2 = 50;
public const RELOGIN_DELAY_2 = 3600; // relogin delay after LOGIN_TRIES_2 tries

private const SESSION_PASSWORD_CHANGE_REQUIRED = 'password_change_required';

/**
* @var string
*/
private $tableName;
/**
* @var bool|null
*/
private $stayLoggedIn;

/** @var rex_backend_password_policy */
Expand All @@ -38,13 +37,15 @@ public function __construct()
$this->setImpersonateQuery($qry . ' WHERE id = :id');
$this->passwordPolicy = rex_backend_password_policy::factory();

$loginPolicy = $this->getLoginPolicy();

// XXX because with concat the time into the sql query, users of this class should use checkLogin() immediately after creating the object.
$qry .= ' WHERE
status = 1
AND login = :login
AND (login_tries < ' . self::LOGIN_TRIES_1 . '
OR login_tries < ' . self::LOGIN_TRIES_2 . ' AND lasttrydate < "' . rex_sql::datetime(time() - self::RELOGIN_DELAY_1) . '"
OR lasttrydate < "' . rex_sql::datetime(time() - self::RELOGIN_DELAY_2) . '"
AND (login_tries < ' . $loginPolicy->getSetting('login_tries_1') . '
OR login_tries < ' . $loginPolicy->getSetting('login_tries_2') . ' AND lasttrydate < "' . rex_sql::datetime(time() - $loginPolicy->getSetting('relogin_delay_1')) . '"
OR lasttrydate < "' . rex_sql::datetime(time() - $loginPolicy->getSetting('relogin_delay_2')) . '"
)';

if ($blockAccountAfter = $this->passwordPolicy->getBlockAccountAfter()) {
Expand All @@ -57,8 +58,16 @@ public function __construct()
$this->tableName = $tableName;
}

/**
* @param bool $stayLoggedIn
* @return void
*/
public function setStayLoggedIn($stayLoggedIn = false)
{
if (!$this->getLoginPolicy()->isStayLoggedInEnabled()) {
$stayLoggedIn = false;
}

$this->stayLoggedIn = $stayLoggedIn;
}

Expand Down Expand Up @@ -131,11 +140,12 @@ public function checkLogin()
if ('' != $this->userLogin) {
$sql->setQuery('SELECT login_tries FROM ' . $this->tableName . ' WHERE login=? LIMIT 1', [$this->userLogin]);
if ($sql->getRows() > 0) {
$loginPolify = $this->getLoginPolicy();

$loginTries = $sql->getValue('login_tries');
$this->increaseLoginTries();

if ($loginTries >= self::LOGIN_TRIES_1 - 1) {
$time = $loginTries < self::LOGIN_TRIES_2 ? self::RELOGIN_DELAY_1 : self::RELOGIN_DELAY_2;
if ($loginTries >= $loginPolify->getSetting('login_tries_1') - 1) {
$time = $loginTries < $loginPolify->getSetting('login_tries_2') ? $loginPolify->getSetting('relogin_delay_1') : $loginPolify->getSetting('relogin_delay_2');
$hours = floor($time / 3600);
$mins = floor(($time - ($hours * 3600)) / 60);
$secs = $time % 60;
Expand Down Expand Up @@ -177,6 +187,9 @@ public function changedPassword(?string $passwordHash = null): void
}
}

/**
* @return void
*/
public static function deleteSession()
{
self::startSession();
Expand Down Expand Up @@ -262,4 +275,12 @@ protected static function getSessionNamespace()
{
return rex::getProperty('instname'). '_backend';
}

public function getLoginPolicy(): rex_login_policy
{
$loginPolicy = (array) rex::getProperty('backend_login_policy', []);

/** @psalm-suppress MixedArgumentTypeCoercion **/
return new rex_login_policy($loginPolicy);
}
}
59 changes: 59 additions & 0 deletions redaxo/src/core/lib/login/login_policy.php
@@ -0,0 +1,59 @@
<?php

/**
* @author mstaab
*
* @package redaxo\core\login
*/
class rex_login_policy
{
/**
* @var array<string, int|bool>
*/
private $options;

/**
* @param array<string, int|bool> $options
*/
public function __construct(array $options)
{
$this->options = $options;
}

/**
* @param 'login_tries_1'|'relogin_delay_1'|'login_tries_2'|'relogin_delay_2' $key
*/
public function getSetting(string $key): int
{
if (array_key_exists($key, $this->options)) {
return (int) $this->options[$key];
}

// defaults, in case config.yml does not define values
// e.g. because of a redaxo core update from a version.
switch ($key) {
case 'login_tries_1':
return 3;
case 'relogin_delay_1':
return 5;
case 'login_tries_2':
return 50;
case 'relogin_delay_2':
return 3600;
}

throw new rex_exception('Invalid login policy key: ' . $key);
}

public function isStayLoggedInEnabled(): bool
{
$key = 'enable_stay_logged_in';

if (array_key_exists($key, $this->options)) {
return (bool) $this->options[$key];
}

// enabled by default
return true;
}
}
10 changes: 6 additions & 4 deletions redaxo/src/core/pages/login.php
Expand Up @@ -85,10 +85,12 @@ function disableLogin() {
$content .= $fragment->parse('core/form/form.php');

$formElements = [];
$n = [];
$n['label'] = '<label for="rex-id-login-stay-logged-in">' . rex_i18n::msg('stay_logged_in') . '</label>';
$n['field'] = '<input type="checkbox" name="rex_user_stay_logged_in" id="rex-id-login-stay-logged-in" value="1" />';
$formElements[] = $n;
if (rex::getProperty('login')->getLoginPolicy()->isStayLoggedInEnabled()) {
$n = [];
$n['label'] = '<label for="rex-id-login-stay-logged-in">' . rex_i18n::msg('stay_logged_in') . '</label>';
$n['field'] = '<input type="checkbox" name="rex_user_stay_logged_in" id="rex-id-login-stay-logged-in" value="1" />';
$formElements[] = $n;
}

$fragment = new rex_fragment();
$fragment->setVar('elements', $formElements, false);
Expand Down
22 changes: 22 additions & 0 deletions redaxo/src/core/schemas/config.json
Expand Up @@ -94,6 +94,28 @@
"description": "Session keep alive (seconds)",
"type": "integer"
},
"backend_login_policy": {
"description": "backend login policy",
"type": "object",
"properties": {
"login_tries_1": {
"type": "integer"
},
"relogin_delay_1": {
"type": "integer"
},
"login_tries_2": {
"type": "integer"
},
"relogin_delay_2": {
"type": "integer"
},
"enable_stay_logged_in": {
"type": "boolean"
}
},
"additionalProperties": false
},
"session": {
"description": "Session configuration",
"type": "object",
Expand Down
3 changes: 2 additions & 1 deletion redaxo/src/core/tests/login/backend_login_test.php
Expand Up @@ -64,8 +64,9 @@ public function testSuccessfullReLogin()
public function testSuccessfullReLoginAfterLoginTries1Seconds()
{
$login = new rex_backend_login();
$tries1 = $login->getLoginPolicy()->getSetting('login_tries_1');

for ($i = 0; $i < rex_backend_login::LOGIN_TRIES_1; ++$i) {
for ($i = 0; $i < $tries1; ++$i) {
$login->setLogin($this->login, 'somethingwhichisnotcorrect', false);
static::assertFalse($login->checkLogin());
}
Expand Down

0 comments on commit b05f1a1

Please sign in to comment.