diff options
author | Stefan Giehl <stefan@matomo.org> | 2019-07-17 07:33:11 +0300 |
---|---|---|
committer | Thomas Steur <tsteur@users.noreply.github.com> | 2019-07-17 07:33:11 +0300 |
commit | ffd09877f0d9ecd56f4baf436979272cba9abbc9 (patch) | |
tree | 7290868f8f3880a8602c6a25ce758ae8ed7488e3 | |
parent | 4506fb9110d920104ab6d4251454b63a3812816a (diff) |
Implements a rate limit for password resets (#13934)
* Implements a rate limit for password resets
* improve error message
* allow up to three requests within one hour
* adds some tests
-rw-r--r-- | plugins/Login/PasswordResetter.php | 26 | ||||
-rw-r--r-- | plugins/Login/lang/en.json | 1 | ||||
-rw-r--r-- | plugins/Login/tests/Integration/PasswordResetterTest.php | 57 |
3 files changed, 83 insertions, 1 deletions
diff --git a/plugins/Login/PasswordResetter.php b/plugins/Login/PasswordResetter.php index f9bd72c476..f1e73cfeee 100644 --- a/plugins/Login/PasswordResetter.php +++ b/plugins/Login/PasswordResetter.php @@ -457,13 +457,37 @@ class PasswordResetter * @param string $login The user login for whom a password change was requested. * @param string $newPassword The new password to set. * @param string $keySuffix The suffix used in generating a token. + * + * @throws Exception if a password reset was already requested within one hour */ private function savePasswordResetInfo($login, $newPassword, $keySuffix) { - $optionName = $this->getPasswordResetInfoOptionName($login); + $optionName = self::getPasswordResetInfoOptionName($login); + + $existingResetInfo = Option::get($optionName); + + $time = time(); + $count = 0; + + if ($existingResetInfo) { + $existingResetInfo = json_decode($existingResetInfo, true); + + if (isset($existingResetInfo['timestamp']) && $existingResetInfo['timestamp'] > time()-3600) { + $time = $existingResetInfo['timestamp']; + $count = !empty($existingResetInfo['requests']) ? $existingResetInfo['requests'] : $count; + + if(isset($existingResetInfo['requests']) && $existingResetInfo['requests'] > 2) { + throw new Exception(Piwik::translate('Login_PasswordResetAlreadySent')); + } + } + } + + $optionData = [ 'hash' => $this->passwordHelper->hash(UsersManager::getPasswordHash($newPassword)), 'keySuffix' => $keySuffix, + 'timestamp' => $time, + 'requests' => $count+1 ]; $optionData = json_encode($optionData); diff --git a/plugins/Login/lang/en.json b/plugins/Login/lang/en.json index e37014a0fd..939941328e 100644 --- a/plugins/Login/lang/en.json +++ b/plugins/Login/lang/en.json @@ -36,6 +36,7 @@ "PasswordChanged": "Your password has been changed.", "PasswordRepeat": "Password (repeat)", "PasswordsDoNotMatch": "Passwords do not match.", + "PasswordResetAlreadySent": "You have requested too many password resets shortly. A new request can be made in one hour. If you have problems resetting your password, please contact you administrator for help.", "WrongPasswordEntered": "Please enter your correct password.", "ConfirmPasswordToContinue": "Confirm your password to continue", "PluginDescription": "Provides authentication via username and password as well as password reset functionality. Authentication method can be changed by using another Login plugin such as LoginLdap available on the Marketplace.", diff --git a/plugins/Login/tests/Integration/PasswordResetterTest.php b/plugins/Login/tests/Integration/PasswordResetterTest.php index f168b15663..2b0af6ac70 100644 --- a/plugins/Login/tests/Integration/PasswordResetterTest.php +++ b/plugins/Login/tests/Integration/PasswordResetterTest.php @@ -13,6 +13,7 @@ use Piwik\Access; use Piwik\Auth; use Piwik\Container\StaticContainer; use Piwik\Mail; +use Piwik\Option; use Piwik\Plugin\Manager; use Piwik\Tests\Framework\TestCase\IntegrationTestCase; use Piwik\Plugins\Login\PasswordResetter; @@ -67,6 +68,62 @@ class PasswordResetterTest extends IntegrationTestCase $this->checkPasswordIs(self::NEWPASSWORD); } + public function tests_passwordReset_worksUpToThreeTimesInAnHour() + { + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + + $this->assertNotEmpty($this->capturedToken); + + $token = $this->capturedToken; + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + $this->assertNotEquals($token, $this->capturedToken); + + $token = $this->capturedToken; + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + $this->assertNotEquals($token, $this->capturedToken); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage You have requested too many password resets shortly. A new request can be made in one hour. If you have problems resetting your password, please contact you administrator for help. + */ + public function test_passwordReset_notAllowedMoreThanThreeTimesInAnHour() + { + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + + $this->assertNotEmpty($this->capturedToken); + + $token = $this->capturedToken; + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + $this->assertNotEquals($token, $this->capturedToken); + + $token = $this->capturedToken; + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + $this->assertNotEquals($token, $this->capturedToken); + + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + } + + public function test_passwordReset_newRequestAllowedAfterAnHour() + { + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + + $optionName = $this->passwordResetter->getPasswordResetInfoOptionName('superUserLogin'); + $data = json_decode(Option::get($optionName), true); + + $data['timestamp'] = time()-3601; + $data['requests'] = 3; + + Option::set($optionName, json_encode($data)); + + $this->passwordResetter->initiatePasswordResetProcess('superUserLogin', self::NEWPASSWORD); + + $optionName = $this->passwordResetter->getPasswordResetInfoOptionName('superUserLogin'); + $data = json_decode(Option::get($optionName), true); + + $this->assertEquals(1, $data['requests']); + } + /** * @expectedException \Exception * @expectedExceptionMessage Token is invalid or has expired |