diff --git a/redaxo/src/core/default.config.yml b/redaxo/src/core/default.config.yml index 5f01fa8eb8..2ed41b526e 100644 --- a/redaxo/src/core/default.config.yml +++ b/redaxo/src/core/default.config.yml @@ -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. diff --git a/redaxo/src/core/lib/login/backend_login.php b/redaxo/src/core/lib/login/backend_login.php index 5445836084..76f415a23f 100644 --- a/redaxo/src/core/lib/login/backend_login.php +++ b/redaxo/src/core/lib/login/backend_login.php @@ -9,10 +9,6 @@ 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'; @@ -20,6 +16,9 @@ class rex_backend_login extends rex_login * @var string */ private $tableName; + /** + * @var bool|null + */ private $stayLoggedIn; /** @var rex_backend_password_policy */ @@ -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()) { @@ -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; } @@ -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; @@ -177,6 +187,9 @@ public function changedPassword(?string $passwordHash = null): void } } + /** + * @return void + */ public static function deleteSession() { self::startSession(); @@ -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); + } } diff --git a/redaxo/src/core/lib/login/login_policy.php b/redaxo/src/core/lib/login/login_policy.php new file mode 100644 index 0000000000..850abce592 --- /dev/null +++ b/redaxo/src/core/lib/login/login_policy.php @@ -0,0 +1,59 @@ + + */ + private $options; + + /** + * @param array $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; + } +} diff --git a/redaxo/src/core/pages/login.php b/redaxo/src/core/pages/login.php index 1e349484ae..ed12cb9d37 100644 --- a/redaxo/src/core/pages/login.php +++ b/redaxo/src/core/pages/login.php @@ -85,10 +85,12 @@ function disableLogin() { $content .= $fragment->parse('core/form/form.php'); $formElements = []; -$n = []; -$n['label'] = ''; -$n['field'] = ''; -$formElements[] = $n; +if (rex::getProperty('login')->getLoginPolicy()->isStayLoggedInEnabled()) { + $n = []; + $n['label'] = ''; + $n['field'] = ''; + $formElements[] = $n; +} $fragment = new rex_fragment(); $fragment->setVar('elements', $formElements, false); diff --git a/redaxo/src/core/schemas/config.json b/redaxo/src/core/schemas/config.json index 00564f10d6..054e5aed23 100644 --- a/redaxo/src/core/schemas/config.json +++ b/redaxo/src/core/schemas/config.json @@ -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", diff --git a/redaxo/src/core/tests/login/backend_login_test.php b/redaxo/src/core/tests/login/backend_login_test.php index 4f5d3a4227..f2dfa00ea9 100644 --- a/redaxo/src/core/tests/login/backend_login_test.php +++ b/redaxo/src/core/tests/login/backend_login_test.php @@ -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()); }