Add time-out to password resets; prevent repeated mails #50
@ -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;
|
||||||
|
2
migrations/2024-11-05.sql
Normal file
2
migrations/2024-11-05.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/* Add time-out to password reset keys, and prevent repeated mails */
|
||||||
|
ALTER TABLE `users` ADD `reset_blocked_until` INT UNSIGNED NULL AFTER `reset_key`;
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user