implement 2fa remember browser, fixes #1259

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann 2024-07-20 10:16:48 +02:00
parent bda24d7d63
commit 2dae780e0b
No known key found for this signature in database
GPG Key ID: C121F97338D7A352
9 changed files with 98 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = '';

View File

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

View File

@ -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' => [

View File

@ -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' => [

View File

@ -22,6 +22,14 @@
<input class="form-control" type="text" name="2fa_code" id="2fa_code" value="" autocomplete="off" autofocus required/>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input type="hidden" name="2fa_remember" value="0"/>
<input class="form-check-input" type="checkbox" id="2fa_remember" name="2fa_remember" value="1">
<label class="form-check-label" for="2fa_remember">{{ lng('login.2faremember') }}</label>
</div>
</div>
</div>
<div class="card-body d-grid gap-2">