1
0
mirror of https://github.com/delight-im/PHP-Auth.git synced 2025-08-03 14:47:30 +02:00

Re-implement internal throttling or rate limiting from scratch

This commit is contained in:
Marco
2017-08-19 00:22:21 +02:00
parent c1bb10f58d
commit a66312bbcf
5 changed files with 190 additions and 178 deletions

View File

@@ -304,6 +304,9 @@ catch (\Delight\Auth\NotLoggedInException $e) {
catch (\Delight\Auth\InvalidPasswordException $e) {
// invalid password(s)
}
catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
Asking the user for their current (and soon *old*) password and requiring it for verification is the recommended way to handle password changes. This is shown above.
@@ -348,6 +351,9 @@ catch (\Delight\Auth\EmailNotVerifiedException $e) {
catch (\Delight\Auth\NotLoggedInException $e) {
// not logged in
}
catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
For email verification, you should build an URL with the selector and token and send it to the user, e.g.:
@@ -541,6 +547,9 @@ try {
catch (\Delight\Auth\NotLoggedInException $e) {
// the user is not signed in
}
catch (\Delight\Auth\TooManyRequestsException $e) {
// too many requests
}
```
### Roles (or groups)

View File

@@ -286,10 +286,6 @@ final class Administration extends UserManager {
return ($rolesBitmask & $role) === $role;
}
protected function throttle($actionType, $customSelector = null) {
// do nothing
}
/**
* Deletes all existing users where the column with the specified name has the given value
*

View File

@@ -30,8 +30,6 @@ final class Auth extends UserManager {
const SESSION_FIELD_REMEMBERED = 'auth_remembered';
const COOKIE_CONTENT_SEPARATOR = '~';
const COOKIE_NAME_REMEMBER = 'auth_remember';
const IP_ADDRESS_HASH_ALGORITHM = 'sha256';
const HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429;
/** @var boolean whether HTTPS (TLS/SSL) will be used (recommended) */
private $useHttps;
@@ -39,10 +37,6 @@ final class Auth extends UserManager {
private $allowCookiesScriptAccess;
/** @var string the user's current IP address */
private $ipAddress;
/** @var int the number of actions allowed (in throttling) per time bucket */
private $throttlingActionsPerTimeBucket;
/** @var int the size of the time buckets (used for throttling) in seconds */
private $throttlingTimeBucketSize;
/**
* @param PdoDatabase|PdoDsn|\PDO $databaseConnection the database connection to operate on
@@ -57,8 +51,6 @@ final class Auth extends UserManager {
$this->useHttps = $useHttps;
$this->allowCookiesScriptAccess = $allowCookiesScriptAccess;
$this->ipAddress = empty($ipAddress) ? $_SERVER['REMOTE_ADDR'] : $ipAddress;
$this->throttlingActionsPerTimeBucket = 20;
$this->throttlingTimeBucketSize = 3600;
$this->initSession();
$this->enhanceHttpSecurity();
@@ -158,13 +150,21 @@ final class Auth extends UserManager {
* @throws InvalidEmailException if the email address was invalid
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see confirmEmail
* @see confirmEmailAndSignIn
*/
public function register($email, $password, $username = null, callable $callback = null) {
return $this->createUserInternal(false, $email, $password, $username, $callback);
$this->throttle([ 'enumerateUsers', $this->getIpAddress() ], 1, (60 * 60), 75);
$this->throttle([ 'createNewAccount', $this->getIpAddress() ], 1, (60 * 60 * 12), 5, true);
$newUserId = $this->createUserInternal(false, $email, $password, $username, $callback);
$this->throttle([ 'createNewAccount', $this->getIpAddress() ], 1, (60 * 60 * 12), 5, false);
return $newUserId;
}
/**
@@ -191,13 +191,21 @@ final class Auth extends UserManager {
* @throws InvalidPasswordException if the password was invalid
* @throws UserAlreadyExistsException if a user with the specified email address already exists
* @throws DuplicateUsernameException if the specified username wasn't unique
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see confirmEmail
* @see confirmEmailAndSignIn
*/
public function registerWithUniqueUsername($email, $password, $username = null, callable $callback = null) {
return $this->createUserInternal(true, $email, $password, $username, $callback);
$this->throttle([ 'enumerateUsers', $this->getIpAddress() ], 1, (60 * 60), 75);
$this->throttle([ 'createNewAccount', $this->getIpAddress() ], 1, (60 * 60 * 12), 5, true);
$newUserId = $this->createUserInternal(true, $email, $password, $username, $callback);
$this->throttle([ 'createNewAccount', $this->getIpAddress() ], 1, (60 * 60 * 12), 5, false);
return $newUserId;
}
/**
@@ -211,9 +219,12 @@ final class Auth extends UserManager {
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function login($email, $password, $rememberDuration = null, callable $onBeforeSuccess = null) {
$this->throttle([ 'attemptToLogin', 'email', $email ], 500, (60 * 60 * 24), null, true);
$this->authenticateUserInternal($password, $email, null, $rememberDuration, $onBeforeSuccess);
}
@@ -233,9 +244,12 @@ final class Auth extends UserManager {
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function loginWithUsername($username, $password, $rememberDuration = null, callable $onBeforeSuccess = null) {
$this->throttle([ 'attemptToLogin', 'username', $username ], 500, (60 * 60 * 24), null, true);
$this->authenticateUserInternal($password, null, $username, $rememberDuration, $onBeforeSuccess);
}
@@ -253,6 +267,7 @@ final class Auth extends UserManager {
* @param string $password the user's password
* @return bool whether the supplied password has been correct
* @throws NotLoggedInException if the user is not currently signed in
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function reconfirmPassword($password) {
@@ -264,6 +279,8 @@ final class Auth extends UserManager {
return false;
}
$this->throttle([ 'reconfirmPassword', $this->getIpAddress() ], 3, (60 * 60), 4, true);
try {
$expectedHash = $this->db->selectValue(
'SELECT password FROM ' . $this->dbTablePrefix . 'users WHERE id = ?',
@@ -275,7 +292,13 @@ final class Auth extends UserManager {
}
if (!empty($expectedHash)) {
return \password_verify($password, $expectedHash);
$validated = \password_verify($password, $expectedHash);
if (!$validated) {
$this->throttle([ 'reconfirmPassword', $this->getIpAddress() ], 3, (60 * 60), 4, false);
}
return $validated;
}
else {
throw new NotLoggedInException();
@@ -481,11 +504,13 @@ final class Auth extends UserManager {
* @throws InvalidSelectorTokenPairException if either the selector or the token was not correct
* @throws TokenExpiredException if the token has already expired
* @throws UserAlreadyExistsException if an attempt has been made to change the email address to a (now) occupied address
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function confirmEmail($selector, $token) {
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN);
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN, $selector);
$this->throttle([ 'confirmEmail', $this->getIpAddress() ], 5, (60 * 60), 10);
$this->throttle([ 'confirmEmail', 'selector', $selector ], 3, (60 * 60), 10);
$this->throttle([ 'confirmEmail', 'token', $token ], 3, (60 * 60), 10);
try {
$confirmationData = $this->db->selectRow(
@@ -566,6 +591,7 @@ final class Auth extends UserManager {
* @throws TokenExpiredException if the token has already expired
* @throws UserAlreadyExistsException if an attempt has been made to change the email address to a (now) occupied address
* @throws InvalidEmailException if the email address has been invalid
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function confirmEmailAndSignIn($selector, $token, $rememberDuration = null) {
@@ -598,6 +624,7 @@ final class Auth extends UserManager {
* @param string $newPassword the new password that should be set
* @throws NotLoggedInException if the user is not currently signed in
* @throws InvalidPasswordException if either the old password has been wrong or the desired new one has been invalid
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function changePassword($oldPassword, $newPassword) {
@@ -668,6 +695,7 @@ final class Auth extends UserManager {
* @throws UserAlreadyExistsException if a user with the desired new email address already exists
* @throws EmailNotVerifiedException if the current (old) email address has not been verified yet
* @throws NotLoggedInException if the user is not currently signed in
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*
* @see confirmEmail
@@ -677,6 +705,8 @@ final class Auth extends UserManager {
if ($this->isLoggedIn()) {
$newEmail = self::validateEmailAddress($newEmail);
$this->throttle([ 'enumerateUsers', $this->getIpAddress() ], 1, (60 * 60), 75);
try {
$existingUsersWithNewEmail = $this->db->selectValue(
'SELECT COUNT(*) FROM ' . $this->dbTablePrefix . 'users WHERE email = ?',
@@ -706,6 +736,9 @@ final class Auth extends UserManager {
throw new EmailNotVerifiedException();
}
$this->throttle([ 'requestEmailChange', $this->getIpAddress() ], 1, (60 * 60 * 24), 3);
$this->throttle([ 'requestEmailChange', 'user', $this->getUserId() ], 1, (60 * 60 * 24), 3);
$this->createConfirmationRequest($this->getUserId(), $newEmail, $callback);
}
else {
@@ -730,6 +763,8 @@ final class Auth extends UserManager {
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
*/
public function resendConfirmationForEmail($email, callable $callback) {
$this->throttle([ 'enumerateUsers', $this->getIpAddress() ], 1, (60 * 60), 75);
$this->resendConfirmationForColumnValue('email', $email, $callback);
}
@@ -791,9 +826,12 @@ final class Auth extends UserManager {
$retryAt = $latestAttempt['expires'] - 0.75 * self::CONFIRMATION_REQUESTS_TTL_IN_SECONDS;
if ($retryAt > \time()) {
self::onTooManyRequests($retryAt - \time());
throw new TooManyRequestsException('', $retryAt - \time());
}
$this->throttle([ 'resendConfirmation', $this->getIpAddress() ], 4, (60 * 60 * 24 * 7), 2);
$this->throttle([ 'resendConfirmation', 'user', $latestAttempt['user_id'] ], 4, (60 * 60 * 24 * 7), 2);
$this->createConfirmationRequest(
$latestAttempt['user_id'],
$latestAttempt['email'],
@@ -825,6 +863,8 @@ final class Auth extends UserManager {
public function forgotPassword($email, callable $callback, $requestExpiresAfter = null, $maxOpenRequests = null) {
$email = self::validateEmailAddress($email);
$this->throttle([ 'enumerateUsers', $this->getIpAddress() ], 1, (60 * 60), 75);
if ($requestExpiresAfter === null) {
// use six hours as the default
$requestExpiresAfter = 60 * 60 * 6;
@@ -859,10 +899,13 @@ final class Auth extends UserManager {
$openRequests = (int) $this->getOpenPasswordResetRequests($userData['id']);
if ($openRequests < $maxOpenRequests) {
$this->throttle([ 'requestPasswordReset', $this->getIpAddress() ], 4, (60 * 60 * 24 * 7), 2);
$this->throttle([ 'requestPasswordReset', 'user', $userData['id'] ], 4, (60 * 60 * 24 * 7), 2);
$this->createPasswordResetRequest($userData['id'], $requestExpiresAfter, $callback);
}
else {
self::onTooManyRequests($requestExpiresAfter);
throw new TooManyRequestsException('', $requestExpiresAfter);
}
}
@@ -880,60 +923,33 @@ final class Auth extends UserManager {
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function authenticateUserInternal($password, $email = null, $username = null, $rememberDuration = null, callable $onBeforeSuccess = null) {
$this->throttle([ 'enumerateUsers', $this->getIpAddress() ], 1, (60 * 60), 75);
$this->throttle([ 'attemptToLogin', $this->getIpAddress() ], 4, (60 * 60), 5, true);
$columnsToFetch = [ 'id', 'email', 'password', 'verified', 'username', 'status', 'roles_mask' ];
if ($email !== null) {
$email = self::validateEmailAddress($email);
// attempt to look up the account information using the specified email address
try {
$userData = $this->getUserDataByEmailAddress(
$email,
$columnsToFetch
);
}
// if there is no user with the specified email address
catch (InvalidEmailException $e) {
// throttle this operation
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
// and re-throw the exception
throw new InvalidEmailException();
}
}
elseif ($username !== null) {
$username = trim($username);
// attempt to look up the account information using the specified username
try {
$userData = $this->getUserDataByUsername(
$username,
$columnsToFetch
);
}
// if there is no user with the specified username
catch (UnknownUsernameException $e) {
// throttle this operation
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle(self::THROTTLE_ACTION_LOGIN, $username);
// and re-throw the exception
throw new UnknownUsernameException();
}
// if there are multiple users with the specified username
catch (AmbiguousUsernameException $e) {
// throttle this operation
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle(self::THROTTLE_ACTION_LOGIN, $username);
// and re-throw the exception
throw new AmbiguousUsernameException();
}
}
// if neither an email address nor a username has been provided
else {
// we can't do anything here because the method call has been invalid
@@ -968,6 +984,15 @@ final class Auth extends UserManager {
return;
}
else {
$this->throttle([ 'attemptToLogin', $this->getIpAddress() ], 4, (60 * 60), 5, false);
if (isset($email)) {
$this->throttle([ 'attemptToLogin', 'email', $email ], 500, (60 * 60 * 24), null, false);
}
elseif (isset($username)) {
$this->throttle([ 'attemptToLogin', 'username', $username ], 500, (60 * 60 * 24), null, false);
}
throw new AttemptCancelledException();
}
}
@@ -976,13 +1001,13 @@ final class Auth extends UserManager {
}
}
else {
// throttle this operation
$this->throttle(self::THROTTLE_ACTION_LOGIN);
$this->throttle([ 'attemptToLogin', $this->getIpAddress() ], 4, (60 * 60), 5, false);
if (isset($email)) {
$this->throttle(self::THROTTLE_ACTION_LOGIN, $email);
$this->throttle([ 'attemptToLogin', 'email', $email ], 500, (60 * 60 * 24), null, false);
}
elseif (isset($username)) {
$this->throttle(self::THROTTLE_ACTION_LOGIN, $username);
$this->throttle([ 'attemptToLogin', 'username', $username ], 500, (60 * 60 * 24), null, false);
}
// we cannot authenticate the user due to the password being wrong
@@ -1111,8 +1136,9 @@ final class Auth extends UserManager {
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function resetPassword($selector, $token, $newPassword) {
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN);
$this->throttle(self::THROTTLE_ACTION_CONSUME_TOKEN, $selector);
$this->throttle([ 'resetPassword', $this->getIpAddress() ], 5, (60 * 60), 10);
$this->throttle([ 'resetPassword', 'selector', $selector ], 3, (60 * 60), 10);
$this->throttle([ 'resetPassword', 'token', $token ], 3, (60 * 60), 10);
try {
$resetData = $this->db->selectRow(
@@ -1536,18 +1562,6 @@ final class Auth extends UserManager {
}
}
/**
* Hashes the supplied data
*
* @param mixed $data the data to hash
* @return string the hash in Base64-encoded format
*/
private static function hash($data) {
$hashRaw = hash(self::IP_ADDRESS_HASH_ALGORITHM, $data, true);
return Base64::encode($hashRaw);
}
/**
* Returns the user's current IP address
*
@@ -1558,114 +1572,113 @@ final class Auth extends UserManager {
}
/**
* Returns the current time bucket that is used for throttling purposes
* Performs throttling or rate limiting using the token bucket algorithm (inverse leaky bucket algorithm)
*
* @return int the time bucket
* @param array $criteria the individual criteria that together describe the resource that is being throttled
* @param int $supply the number of units to provide per interval (>= 1)
* @param int $interval the interval (in seconds) for which the supply is provided (>= 5)
* @param int|null $burstiness (optional) the permitted degree of variation or unevenness during peaks (>= 1)
* @param bool|null $simulated (optional) whether to simulate a dry run instead of actually consuming the requested units
* @param int|null $cost (optional) the number of units to request (>= 1)
* @return float the number of units remaining from the supply
* @throws TooManyRequestsException if the actual demand has exceeded the designated supply
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function getTimeBucket() {
return (int) (time() / $this->throttlingTimeBucketSize);
protected function throttle(array $criteria, $supply, $interval, $burstiness = null, $simulated = null, $cost = null) {
// generate a unique key for the bucket (consisting of 44 or fewer ASCII characters)
$key = Base64::encodeUrlSafeWithoutPadding(
\hash(
'sha256',
\implode("\n", $criteria),
true
)
);
// validate the supplied parameters and set appropriate defaults where necessary
$burstiness = ($burstiness !== null) ? (int) $burstiness : 1;
$simulated = ($simulated !== null) ? (bool) $simulated : false;
$cost = ($cost !== null) ? (int) $cost : 1;
$now = \time();
// determine the volume of the bucket
$capacity = $burstiness * (int) $supply;
// calculate the rate at which the bucket is refilled (per second)
$bandwidthPerSecond = (int) $supply / (int) $interval;
try {
$bucket = $this->db->selectRow(
'SELECT tokens, replenished_at FROM ' . $this->dbTablePrefix . 'users_throttling WHERE bucket = ?',
[ $key ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
protected function throttle($actionType, $customSelector = null) {
// if a custom selector has been provided (e.g. username, user ID or confirmation token)
if (isset($customSelector)) {
// use the provided selector for throttling
$selector = self::hash($customSelector);
}
// if no custom selector was provided
else {
// throttle by the user's IP address
$selector = self::hash($this->getIpAddress());
if ($bucket === null) {
$bucket = [];
}
// get the time bucket that we do the throttling for
$timeBucket = self::getTimeBucket();
// initialize the number of tokens in the bucket
$bucket['tokens'] = isset($bucket['tokens']) ? (float) $bucket['tokens'] : (float) $capacity;
// initialize the last time that the bucket has been refilled (as a Unix timestamp in seconds)
$bucket['replenished_at'] = isset($bucket['replenished_at']) ? (int) $bucket['replenished_at'] : $now;
// replenish the bucket as appropriate
$secondsSinceLastReplenishment = \max(0, $now - $bucket['replenished_at']);
$tokensToAdd = $secondsSinceLastReplenishment * $bandwidthPerSecond;
$bucket['tokens'] = \min((float) $capacity, $bucket['tokens'] + $tokensToAdd);
$bucket['replenished_at'] = $now;
$accepted = $bucket['tokens'] >= $cost;
if (!$simulated) {
if ($accepted) {
// remove the requested number of tokens from the bucket
$bucket['tokens'] = \max(0, $bucket['tokens'] - $cost);
}
// set the earliest time after which the bucket *may* be deleted (as a Unix timestamp in seconds)
$bucket['expires_at'] = $now + \floor($capacity / $bandwidthPerSecond * 2);
// merge the updated bucket into the database
try {
$affected = $this->db->update(
$this->dbTablePrefix . 'users_throttling',
$bucket,
[ 'bucket' => $key ]
);
}
catch (Error $e) {
throw new DatabaseError();
}
if ($affected === 0) {
$bucket['bucket'] = $key;
try {
$this->db->insert(
$this->dbTablePrefix . 'users_throttling',
[
'action_type' => $actionType,
'selector' => $selector,
'time_bucket' => $timeBucket,
'attempts' => 1
]
);
}
catch (IntegrityConstraintViolationException $e) {
// if we have a duplicate entry, update the old entry
try {
$this->db->exec(
'UPDATE ' . $this->dbTablePrefix . 'users_throttling SET attempts = attempts+1 WHERE action_type = ? AND selector = ? AND time_bucket = ?',
[
$actionType,
$selector,
$timeBucket
]
$bucket
);
}
catch (IntegrityConstraintViolationException $ignored) {}
catch (Error $e) {
throw new DatabaseError();
}
}
catch (Error $e) {
throw new DatabaseError();
}
try {
$attempts = $this->db->selectValue(
'SELECT attempts FROM ' . $this->dbTablePrefix . 'users_throttling WHERE action_type = ? AND selector = ? AND time_bucket = ?',
[
$actionType,
$selector,
$timeBucket
]
);
}
catch (Error $e) {
throw new DatabaseError();
if ($accepted) {
return $bucket['tokens'];
}
else {
$tokensMissing = $cost - $bucket['tokens'];
$estimatedWaitingTimeSeconds = \ceil($tokensMissing / $bandwidthPerSecond);
if (!empty($attempts)) {
// if the number of attempts has acceeded our accepted limit
if ($attempts > $this->throttlingActionsPerTimeBucket) {
self::onTooManyRequests($this->throttlingTimeBucketSize);
}
}
}
/**
* Called when there have been too many requests for some action or object
*
* @param int|null $retryAfterInterval (optional) the interval in seconds after which the client should retry
* @throws TooManyRequestsException to inform any calling method about this problem
*/
private static function onTooManyRequests($retryAfterInterval = null) {
// if no interval has been provided after which the client should retry
if ($retryAfterInterval === null) {
// use one day as the default
$retryAfterInterval = 60 * 60 * 24;
}
// send an appropriate HTTP status code
http_response_code(self::HTTP_STATUS_CODE_TOO_MANY_REQUESTS);
// tell the client when they should try again
@header('Retry-After: '.$retryAfterInterval);
// throw an exception
throw new TooManyRequestsException();
}
/**
* Customizes the throttling options
*
* @param int $actionsPerTimeBucket the number of allowed attempts/requests per time bucket
* @param int $timeBucketSize the size of the time buckets in seconds
*/
public function setThrottlingOptions($actionsPerTimeBucket, $timeBucketSize) {
$this->throttlingActionsPerTimeBucket = intval($actionsPerTimeBucket);
if (isset($timeBucketSize)) {
$this->throttlingTimeBucketSize = intval($timeBucketSize);
throw new TooManyRequestsException('', $estimatedWaitingTimeSeconds);
}
}

View File

@@ -23,9 +23,6 @@ require_once __DIR__ . '/Exceptions.php';
*/
abstract class UserManager {
const THROTTLE_ACTION_LOGIN = 'login';
const THROTTLE_ACTION_REGISTER = 'register';
const THROTTLE_ACTION_CONSUME_TOKEN = 'confirm_email';
const CONFIRMATION_REQUESTS_TTL_IN_SECONDS = 60 * 60 * 24;
/** @var PdoDatabase the database connection to operate on */
@@ -106,8 +103,6 @@ abstract class UserManager {
* @see confirmEmailAndSignIn
*/
protected function createUserInternal($requireUniqueUsername, $email, $password, $username = null, callable $callback = null) {
$this->throttle(self::THROTTLE_ACTION_REGISTER);
ignore_user_abort(true);
$email = self::validateEmailAddress($email);
@@ -251,16 +246,6 @@ abstract class UserManager {
return $password;
}
/**
* Throttles the specified action for the user to protect against too many requests
*
* @param string $actionType one of the constants from this class starting with `THROTTLE_ACTION_`
* @param mixed|null $customSelector a custom selector to use for throttling (if any), otherwise the IP address will be used
* @throws TooManyRequestsException if the number of allowed attempts/requests has been exceeded
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
abstract protected function throttle($actionType, $customSelector = null);
/**
* Creates a request for email confirmation
*

View File

@@ -282,6 +282,9 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'changePassword') {
try {
@@ -295,6 +298,9 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\InvalidPasswordException $e) {
return 'invalid password(s)';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'changePasswordWithoutOldPassword') {
try {
@@ -339,6 +345,9 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\NotLoggedInException $e) {
return 'not logged in';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
}
else if ($_POST['action'] === 'setPasswordResetEnabled') {
try {