verify 2FA code once before storing secret and activation for login to be sure it works; fixes #1030

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2022-05-15 12:27:48 +02:00
parent dd896659ae
commit a5115414a8
3 changed files with 77 additions and 15 deletions

73
2fa.php
View File

@@ -34,6 +34,7 @@ use Froxlor\FroxlorTwoFactorAuth;
use Froxlor\Settings; use Froxlor\Settings;
use Froxlor\UI\Panel\UI; use Froxlor\UI\Panel\UI;
use Froxlor\UI\Response; use Froxlor\UI\Response;
use Froxlor\PhpHelper;
if (Settings::Get('2fa.enabled') != '1') { if (Settings::Get('2fa.enabled') != '1') {
Response::dynamicError('2fa.2fa_not_activated'); Response::dynamicError('2fa.2fa_not_activated');
@@ -60,22 +61,71 @@ if ($action == 'delete') {
'id' => $uid 'id' => $uid
]); ]);
Response::standardSuccess('2fa.2fa_removed'); Response::standardSuccess('2fa.2fa_removed');
} elseif ($action == 'add') { } elseif ($action == 'preadd') {
$type = isset($_POST['type_2fa']) ? $_POST['type_2fa'] : '0'; $type = isset($_POST['type_2fa']) ? $_POST['type_2fa'] : '0';
if ($type == 0 || $type == 1) { $data = "";
$data = ""; if ($type > 0) {
}
if ($type == 2) {
// generate secret for TOTP // generate secret for TOTP
$data = $tfa->createSecret(); $data = $tfa->createSecret();
$userinfo['type_2fa'] = $type;
$userinfo['data_2fa'] = $data;
$userinfo['2fa_unsaved'] = true;
// if type = email, send a code there for confirmation
if ($type == 1) {
$code = $tfa->getCode($tfa->createSecret());
$_mailerror = false;
$mailerr_msg = "";
$replace_arr = [
'CODE' => $code
];
$mail_body = html_entity_decode(PhpHelper::replaceVariables(lng('mails.2fa.mailbody'), $replace_arr));
try {
$mail->Subject = lng('mails.2fa.subject');
$mail->AltBody = $mail_body;
$mail->MsgHTML(str_replace("\n", "<br />", $mail_body));
$mail->AddAddress($userinfo['email'], User::getCorrectUserSalutation($userinfo));
$mail->Send();
} catch (\PHPMailer\PHPMailer\Exception $e) {
$mailerr_msg = $e->errorMessage();
$_mailerror = true;
} catch (Exception $e) {
$mailerr_msg = $e->getMessage();
$_mailerror = true;
}
if ($_mailerror) {
Response::dynamicError($mailerr_msg);
}
}
UI::twig()->addGlobal('userinfo', $userinfo);
} else {
Response::dynamicError('Select one of the possible values for 2FA');
} }
Database::pexecute($upd_stmt, [ } elseif ($action == 'add') {
't2fa' => $type, $type = isset($_POST['type_2fa']) ? $_POST['type_2fa'] : '0';
'd2fa' => $data, $data = isset($_POST['data_2fa']) ? $_POST['data_2fa'] : '';
'id' => $uid $code = isset($_POST['codevalidation']) ? $_POST['codevalidation'] : '';
]);
Response::standardSuccess('2fa.2fa_added', [$filename]); // validate
$result = $tfa->verifyCode($data, $code, 3);
if ($result) {
if ($type == 0 || $type == 1) {
// no fixed secret for email validation, the validation code will be set on the fly
$data = "";
}
Database::pexecute($upd_stmt, [
't2fa' => $type,
'd2fa' => $data,
'id' => $uid
]);
Response::standardSuccess('2fa.2fa_added', $filename);
}
Response::dynamicError('Invalid/wrong code');
} }
$log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed 2fa::overview"); $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed 2fa::overview");
@@ -98,7 +148,6 @@ if ($userinfo['type_2fa'] == '0') {
} }
UI::view('user/2fa.html.twig', [ UI::view('user/2fa.html.twig', [
'themes' => $themes_avail,
'type_select_values' => $type_select_values, 'type_select_values' => $type_select_values,
'ga_qrcode' => $ga_qrcode 'ga_qrcode' => $ga_qrcode
]); ]);

View File

@@ -65,7 +65,7 @@ if ($action == '2fa_entercode') {
} }
$code = isset($_POST['2fa_code']) ? $_POST['2fa_code'] : null; $code = isset($_POST['2fa_code']) ? $_POST['2fa_code'] : null;
// verify entered code // verify entered code
$tfa = new FroxlorTwoFactorAuth('Froxlor'); $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname'));
$result = ($_SESSION['secret_2fa'] == 'email' ? true : $tfa->verifyCode($_SESSION['secret_2fa'], $code, 3)); $result = ($_SESSION['secret_2fa'] == 'email' ? true : $tfa->verifyCode($_SESSION['secret_2fa'], $code, 3));
// either the code is valid when using authenticator-app, or we will select userdata by id and entered code // either the code is valid when using authenticator-app, or we will select userdata by id and entered code
// which is temporarily stored for the customer when using email-2fa // which is temporarily stored for the customer when using email-2fa
@@ -283,7 +283,7 @@ if ($action == '2fa_entercode') {
// send mail if type_2fa = 1 (email) // send mail if type_2fa = 1 (email)
if ($userinfo['type_2fa'] == 1) { if ($userinfo['type_2fa'] == 1) {
// generate code // generate code
$tfa = new FroxlorTwoFactorAuth('Froxlor'); $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname'));
$code = $tfa->getCode($tfa->createSecret()); $code = $tfa->getCode($tfa->createSecret());
// set code for user // set code for user
$stmt = Database::prepare("UPDATE $table SET `data_2fa` = :d2fa WHERE `$uid` = :uid"); $stmt = Database::prepare("UPDATE $table SET `data_2fa` = :d2fa WHERE `$uid` = :uid");

View File

@@ -4,6 +4,8 @@
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
{% if userinfo.type_2fa == 0 %} {% if userinfo.type_2fa == 0 %}
{% set linkeraction = 'preadd' %}
{% elseif userinfo['2fa_unsaved'] is defined and userinfo['2fa_unsaved'] %}
{% set linkeraction = 'add' %} {% set linkeraction = 'add' %}
{% else %} {% else %}
{% set linkeraction = 'delete' %} {% set linkeraction = 'delete' %}
@@ -24,9 +26,17 @@
{% elseif userinfo.type_2fa == 2 %} {% elseif userinfo.type_2fa == 2 %}
<label for="qrcode" class="col-form-label">{{ lng('2fa.2fa_ga_desc')|raw }}</label> <label for="qrcode" class="col-form-label">{{ lng('2fa.2fa_ga_desc')|raw }}</label>
<img src="{{ ga_qrcode }}" class="img-fluid" alt="QRCode" id="qrcode"/> <img src="{{ ga_qrcode }}" class="img-fluid" alt="QRCode" id="qrcode"/><br>
<span>Code: <code>{{ userinfo.data_2fa }}</code></span>
{% endif %} {% endif %}
{% if userinfo['2fa_unsaved'] is defined and userinfo['2fa_unsaved'] %}
<br>
<label for="codevalidation" class="col-form-label">{{ lng('login.2facode') }}</label>
<input type="text" name="codevalidation" id="codevalidation" class="form-control" required/>
<input type="hidden" name="type_2fa" id="type_2fa" value="{{ userinfo.type_2fa }}"/>
<input type="hidden" name="data_2fa" id="data_2fa" value="{{ userinfo.data_2fa }}"/>
{% endif %}
</div> </div>
</div> </div>
@@ -34,6 +44,9 @@
<input type="hidden" name="page" value="{{ page }}"/> <input type="hidden" name="page" value="{{ page }}"/>
<input type="hidden" name="send" value="send"/> <input type="hidden" name="send" value="send"/>
{% if userinfo.type_2fa == 0 %} {% if userinfo.type_2fa == 0 %}
<button class="btn btn-primary rounded-top-0" type="submit" name="preadd">
{{ lng('2fa.2fa_add') }}</button>
{% elseif userinfo['2fa_unsaved'] is defined and userinfo['2fa_unsaved'] %}
<button class="btn btn-primary rounded-top-0" type="submit" name="add"> <button class="btn btn-primary rounded-top-0" type="submit" name="add">
{{ lng('2fa.2fa_add') }}</button> {{ lng('2fa.2fa_add') }}</button>
{% else %} {% else %}