diff --git a/index.php b/index.php index 205b0517..dd3f2673 100644 --- a/index.php +++ b/index.php @@ -72,6 +72,7 @@ if ($action == '2fa_entercode') { exit(); } $code = Request::post('2fa_code'); + $remember = Request::post('2fa_remember'); // verify entered code $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname')); // get user-data @@ -105,13 +106,6 @@ if ($action == '2fa_entercode') { $userinfo['adminsession'] = $isadmin; $userinfo['userid'] = $uid; - // if not successful somehow - start again - if (!finishLogin($userinfo)) { - Response::redirectTo('index.php', [ - 'showmessage' => '2' - ]); - } - // when using email-2fa, remove the one-time-code if ($userinfo['type_2fa'] == '1') { $del_stmt = Database::prepare("UPDATE " . $table . " SET `data_2fa` = '' WHERE `" . $field . "` = :uid"); @@ -119,6 +113,42 @@ if ($action == '2fa_entercode') { 'uid' => $uid ]); } + + // when remember is activated, set the cookie + if ($remember) { + $selector = base64_encode(Froxlor::genSessionId(9)); + $authenticator = Froxlor::genSessionId(33); + $valid_until = time()+60*60*24*30; + $ins_stmt = Database::prepare(" + INSERT INTO `".TABLE_PANEL_2FA_TOKENS."` SET + `selector` = :selector, + `token` = :authenticator, + `userid` = :userid, + `valid_until` = :valid_until + "); + Database::pexecute($ins_stmt, [ + 'selector' => $selector, + 'authenticator' => hash('sha256', $authenticator), + 'userid' => $uid, + 'valid_until' => $valid_until + ]); + $cookie_params = [ + 'expires' => $valid_until, // 30 days + 'path' => '/', + 'domain' => UI::getCookieHost(), + 'secure' => UI::requestIsHttps(), + 'httponly' => true, + 'samesite' => 'Strict' + ]; + setcookie('frx_2fa_remember', $selector.':'.base64_encode($authenticator), $cookie_params); + } + + // if not successful somehow - start again + if (!finishLogin($userinfo)) { + Response::redirectTo('index.php', [ + 'showmessage' => '2' + ]); + } exit(); } // wrong 2fa code - treat like "wrong password" @@ -349,6 +379,22 @@ if ($action == '2fa_entercode') { // 2FA activated if (Settings::Get('2fa.enabled') == '1' && $userinfo['type_2fa'] > 0) { + + // check for remember cookie + if (!empty($_COOKIE['frx_2fa_remember'])) { + list($selector, $authenticator) = explode(':', $_COOKIE['frx_2fa_remember']); + $sel_stmt = Database::prepare("SELECT `token` FROM `".TABLE_PANEL_2FA_TOKENS."` WHERE `selector` = :selector AND `userid` = :uid AND `valid_until` >= UNIX_TIMESTAMP()"); + $token_check = Database::pexecute_first($sel_stmt, ['selector' => $selector, 'uid' => $userinfo[$uid]]); + if ($token_check && hash_equals($token_check['token'], hash('sha256', base64_decode($authenticator)))) { + if (!finishLogin($userinfo)) { + Response::redirectTo('index.php', [ + 'showmessage' => '2' + ]); + } + exit(); + } + } + // redirect to code-enter-page $_SESSION['secret_2fa'] = ($userinfo['type_2fa'] == 2 ? $userinfo['data_2fa'] : 'email'); $_SESSION['uid_2fa'] = $userinfo[$uid]; @@ -829,8 +875,8 @@ function finishLogin($userinfo) $theme = $userinfo['theme']; } else { $theme = Settings::Get('panel.default_theme'); - CurrentUser::setField('theme', $theme); } + CurrentUser::setField('theme', $theme); $qryparams = []; if (!empty($_SESSION['lastqrystr'])) { diff --git a/install/froxlor.sql.php b/install/froxlor.sql.php index 299560de..4c980b21 100644 --- a/install/froxlor.sql.php +++ b/install/froxlor.sql.php @@ -731,7 +731,7 @@ opcache.validate_timestamps'), ('panel', 'settings_mode', '0'), ('panel', 'menu_collapsed', '1'), ('panel', 'version', '2.2.0-rc1'), - ('panel', 'db_version', '202401090'); + ('panel', 'db_version', '202407200'); DROP TABLE IF EXISTS `panel_tasks`; @@ -1049,4 +1049,15 @@ CREATE TABLE `panel_loginlinks` ( `allowed_from` text NOT NULL, UNIQUE KEY `loginname` (`loginname`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; + + +DROP TABLE IF EXISTS `panel_2fa_tokens`; +CREATE TABLE `panel_2fa_tokens` ( + `id` int(11) NOT NULL auto_increment, + `selector` varchar(20) NOT NULL, + `token` varchar(200) NOT NULL, + `userid` int(11) NOT NULL default '0', + `valid_until` int(15) NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; FROXLORSQL; diff --git a/install/updates/froxlor/update_2.2.inc.php b/install/updates/froxlor/update_2.2.inc.php index 9e96a98a..61883f6a 100644 --- a/install/updates/froxlor/update_2.2.inc.php +++ b/install/updates/froxlor/update_2.2.inc.php @@ -122,3 +122,21 @@ if (Froxlor::isFroxlorVersion('2.2.0-dev1')) { Update::showUpdateStep("Updating from 2.2.0-dev1 to 2.2.0-rc1", false); Froxlor::updateToVersion('2.2.0-rc1'); } + +if (Froxlor::isDatabaseVersion('202401090')) { + + Update::showUpdateStep("Adding new table for 2fa tokens"); + Database::query("DROP TABLE IF EXISTS `panel_2fa_tokens`;"); + $sql = "CREATE TABLE `panel_2fa_tokens` ( + `id` int(11) NOT NULL auto_increment, + `selector` varchar(20) NOT NULL, + `token` varchar(200) NOT NULL, + `userid` int(11) NOT NULL default '0', + `valid_until` int(15) NOT NULL, + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"; + Database::query($sql); + Update::lastStepStatus(0); + + Froxlor::updateToDbVersion('202407200'); +} diff --git a/lib/Froxlor/Cli/MasterCron.php b/lib/Froxlor/Cli/MasterCron.php index 61926ecc..7045de8e 100644 --- a/lib/Froxlor/Cli/MasterCron.php +++ b/lib/Froxlor/Cli/MasterCron.php @@ -171,8 +171,9 @@ final class MasterCron extends CliCommand FroxlorLogger::getInstanceOf()->setCronLog(0); } - // clean up possible old login-links + // clean up possible old login-links and 2fa tokens Database::query("DELETE FROM `" . TABLE_PANEL_LOGINLINKS . "` WHERE `valid_until` < UNIX_TIMESTAMP()"); + Database::query("DELETE FROM `" . TABLE_PANEL_2FA_TOKENS . "` WHERE `valid_until` < UNIX_TIMESTAMP()"); return $result; } diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php index 83bc5501..ce905371 100644 --- a/lib/Froxlor/Froxlor.php +++ b/lib/Froxlor/Froxlor.php @@ -34,7 +34,7 @@ final class Froxlor const VERSION = '2.2.0-rc1'; // Database version (YYYYMMDDC where C is a daily counter) - const DBVERSION = '202401090'; + const DBVERSION = '202407200'; // Distribution branding-tag (used for Debian etc.) const BRANDING = ''; diff --git a/lib/tables.inc.php b/lib/tables.inc.php index 26214333..0805f290 100644 --- a/lib/tables.inc.php +++ b/lib/tables.inc.php @@ -57,3 +57,4 @@ const TABLE_PANEL_PLANS = 'panel_plans'; const TABLE_API_KEYS = 'api_keys'; const TABLE_PANEL_USERCOLUMNS = 'panel_usercolumns'; const TABLE_PANEL_LOGINLINKS = 'panel_loginlinks'; +const TABLE_PANEL_2FA_TOKENS = 'panel_2fa_tokens'; diff --git a/lng/de.lng.php b/lng/de.lng.php index 119cb0d0..841d9fe5 100644 --- a/lng/de.lng.php +++ b/lng/de.lng.php @@ -1032,6 +1032,7 @@ return [ 'combination_not_found' => 'Kombination aus Benutzername und E-Mail Adresse stimmen nicht überein.', '2fa' => 'Zwei-Faktor Authentifizierung (2FA)', '2facode' => 'Bitte 2FA Code angeben', + '2faremember' => 'Browser vertrauen', ], 'mails' => [ 'pop_success' => [ diff --git a/lng/en.lng.php b/lng/en.lng.php index d1cbb0c4..31035ed6 100644 --- a/lng/en.lng.php +++ b/lng/en.lng.php @@ -1104,6 +1104,7 @@ return [ 'combination_not_found' => 'Combination of user and email address not found.', '2fa' => 'Two-factor authentication (2FA)', '2facode' => 'Please enter 2FA code', + '2faremember' => 'Trust browser', ], 'mails' => [ 'pop_success' => [ diff --git a/templates/Froxlor/login/enter2fa.html.twig b/templates/Froxlor/login/enter2fa.html.twig index b435bb0b..37cc3d66 100644 --- a/templates/Froxlor/login/enter2fa.html.twig +++ b/templates/Froxlor/login/enter2fa.html.twig @@ -22,6 +22,14 @@ +
+
+ + + +
+
+