ResetPassword: add time-out to password resets; prevent repeated mails

This commit is contained in:
Aaron van Geffen 2024-11-05 17:19:59 +01:00
parent eb7a40a70d
commit f511e678ca
3 changed files with 70 additions and 1 deletions

View File

@ -37,6 +37,24 @@ class ResetPassword extends HTMLController
return; return;
} }
if (Authentication::getResetTimeOut($user->getUserId()) > 0)
{
// Update the reset time-out to prevent hammering
$resetTimeOut = Authentication::updateResetTimeOut($user->getUserId());
// Present it to the user in a readable way
if ($resetTimeOut > 3600)
$timeOut = sprintf('%d hours', ceil($resetTimeOut / 3600));
elseif ($resetTimeOut > 60)
$timeOut = sprintf('%d minutes', ceil($resetTimeOut / 60));
else
$timeOut = sprintf('%d seconds', $resetTimeOut);
$form->adopt(new Alert('Password reset token already sent', 'We already sent a password reset token to this email address recently. ' .
'If no email was received, please wait ' . $timeOut . ' to try again.', 'error'));
return;
}
Authentication::setResetKey($user->getUserId()); Authentication::setResetKey($user->getUserId());
Email::resetMail($user->getUserId()); Email::resetMail($user->getUserId());
@ -76,6 +94,10 @@ class ResetPassword extends HTMLController
if (empty($missing)) if (empty($missing))
{ {
Authentication::updatePassword($user->getUserId(), Authentication::computeHash($_POST['password1'])); Authentication::updatePassword($user->getUserId(), Authentication::computeHash($_POST['password1']));
// Consume token, ensuring it isn't used again
Authentication::consumeResetKey($user->getUserId());
$_SESSION['login_msg'] = ['Your password has been reset', 'You can now use the form below to log in to your account.', 'success']; $_SESSION['login_msg'] = ['Your password has been reset', 'You can now use the form below to log in to your account.', 'success'];
header('Location: ' . BASEURL . '/login/'); header('Location: ' . BASEURL . '/login/');
exit; exit;

View File

@ -12,6 +12,8 @@
*/ */
class Authentication class Authentication
{ {
const DEFAULT_RESET_TIMEOUT = 30;
/** /**
* Checks a password for a given username against the database. * Checks a password for a given username against the database.
*/ */
@ -57,6 +59,27 @@ class Authentication
return $hash; return $hash;
} }
public static function consumeResetKey($id_user)
{
return Registry::get('db')->query('
UPDATE users
SET reset_key = NULL,
reset_blocked_until = NULL
WHERE id_user = {int:id_user}',
['id_user' => $id_user]);
}
public static function getResetTimeOut($id_user)
{
$resetTime = Registry::get('db')->queryValue('
SELECT reset_blocked_until
FROM users
WHERE id_user = {int:id_user}',
['id_user' => $id_user]);
return max(0, $resetTime - time());
}
/** /**
* Verifies whether the user is currently logged in. * Verifies whether the user is currently logged in.
*/ */
@ -92,7 +115,8 @@ class Authentication
{ {
return Registry::get('db')->query(' return Registry::get('db')->query('
UPDATE users UPDATE users
SET reset_key = {string:key} SET reset_key = {string:key},
reset_blocked_until = UNIX_TIMESTAMP() + ' . static::DEFAULT_RESET_TIMEOUT . '
WHERE id_user = {int:id}', WHERE id_user = {int:id}',
[ [
'id' => $id_user, 'id' => $id_user,
@ -117,4 +141,26 @@ class Authentication
'blank' => '', 'blank' => '',
]); ]);
} }
public static function updateResetTimeOut($id_user)
{
$currentResetTimeOut = static::getResetTimeOut($id_user);
// New timeout: between 30 seconds, double the current timeout, and a full day
$newResetTimeOut = min(max(static::DEFAULT_RESET_TIMEOUT, $currentResetTimeOut * 2), 60 * 60 * 24);
$success = Registry::get('db')->query('
UPDATE users
SET reset_blocked_until = {int:new_time_out}
WHERE id_user = {int:id_user}',
[
'id_user' => $id_user,
'new_time_out' => time() + $newResetTimeOut,
]);
if (!$success)
throw new UnexpectedValueException('Could not set password reset timeout!');
return $newResetTimeOut;
}
} }

View File

@ -23,6 +23,7 @@ abstract class User
protected $ip_address; protected $ip_address;
protected $is_admin; protected $is_admin;
protected $reset_key; protected $reset_key;
protected $reset_blocked_until;
protected bool $is_logged; protected bool $is_logged;
protected bool $is_guest; protected bool $is_guest;