mirror of
https://github.com/flarum/framework.git
synced 2025-03-21 12:35:15 +08:00
feat: throttle email change, email confirmation, and password reset endpoints. (#3555)
* chore: move post throttler to separate class * feat: throttle email change requests * feat: throttle email activation requests * feat: throttle password resets for logged-in users * docs: comment new throttlers
This commit is contained in:
parent
021793fc52
commit
f610f8aa67
39
framework/core/src/Post/PostCreationThrottler.php
Normal file
39
framework/core/src/Post/PostCreationThrottler.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Post;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class PostCreationThrottler
|
||||
{
|
||||
public static $timeout = 10;
|
||||
|
||||
/**
|
||||
* @return bool|void
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request)
|
||||
{
|
||||
if (! in_array($request->getAttribute('routeName'), ['discussions.create', 'posts.create'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if ($actor->can('postWithoutThrottle')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Post::where('user_id', $actor->id)->where('created_at', '>=', Carbon::now()->subSeconds(self::$timeout))->exists()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,11 +9,10 @@
|
||||
|
||||
namespace Flarum\Post;
|
||||
|
||||
use DateTime;
|
||||
use Flarum\Formatter\Formatter;
|
||||
use Flarum\Foundation\AbstractServiceProvider;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\Post\Access\ScopePostVisibility;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
||||
class PostServiceProvider extends AbstractServiceProvider
|
||||
{
|
||||
@ -22,22 +21,8 @@ class PostServiceProvider extends AbstractServiceProvider
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->container->extend('flarum.api.throttlers', function ($throttlers) {
|
||||
$throttlers['postTimeout'] = function ($request) {
|
||||
if (! in_array($request->getAttribute('routeName'), ['discussions.create', 'posts.create'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if ($actor->can('postWithoutThrottle')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Post::where('user_id', $actor->id)->where('created_at', '>=', new DateTime('-10 seconds'))->exists()) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
$this->container->extend('flarum.api.throttlers', function (array $throttlers, Container $container) {
|
||||
$throttlers['postTimeout'] = $container->make(PostCreationThrottler::class);
|
||||
|
||||
return $throttlers;
|
||||
});
|
||||
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Throttler;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\EmailToken;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Unactivated users can request a confirmation email,
|
||||
* this throttler applies a timeout of 5 minutes between confirmation requests.
|
||||
*/
|
||||
class EmailActivationThrottler
|
||||
{
|
||||
public static $timeout = 300;
|
||||
|
||||
/**
|
||||
* @return bool|void
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request)
|
||||
{
|
||||
if ($request->getAttribute('routeName') !== 'users.confirmation.send') {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if (EmailToken::query()
|
||||
->where('user_id', $actor->id)
|
||||
->where('email', $actor->email)
|
||||
->where('created_at', '>=', Carbon::now()->subSeconds(self::$timeout))
|
||||
->exists()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
46
framework/core/src/User/Throttler/EmailChangeThrottler.php
Normal file
46
framework/core/src/User/Throttler/EmailChangeThrottler.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Throttler;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\EmailToken;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Users can request an email change,
|
||||
* this throttler applies a timeout of 5 minutes between requests.
|
||||
*/
|
||||
class EmailChangeThrottler
|
||||
{
|
||||
public static $timeout = 300;
|
||||
|
||||
/**
|
||||
* @return bool|void
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request)
|
||||
{
|
||||
if ($request->getAttribute('routeName') !== 'users.update') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Arr::has($request->getParsedBody(), 'data.attributes.email')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
// Check that an email token was not already created recently (last 5 minutes).
|
||||
if (EmailToken::query()->where('user_id', $actor->id)->where('created_at', '>=', Carbon::now()->subSeconds(self::$timeout))->exists()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
46
framework/core/src/User/Throttler/PasswordResetThrottler.php
Normal file
46
framework/core/src/User/Throttler/PasswordResetThrottler.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\User\Throttler;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Http\RequestUtil;
|
||||
use Flarum\User\PasswordToken;
|
||||
use Illuminate\Support\Arr;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Logged-in users can request password reset email,
|
||||
* this throttler applies a timeout of 5 minutes between password resets.
|
||||
* This does not apply to guests requesting password resets.
|
||||
*/
|
||||
class PasswordResetThrottler
|
||||
{
|
||||
public static $timeout = 300;
|
||||
|
||||
/**
|
||||
* @return bool|void
|
||||
*/
|
||||
public function __invoke(ServerRequestInterface $request)
|
||||
{
|
||||
if ($request->getAttribute('routeName') !== 'forgot') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Arr::has($request->getParsedBody(), 'email')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = RequestUtil::getActor($request);
|
||||
|
||||
if (PasswordToken::query()->where('user_id', $actor->id)->where('created_at', '>=', Carbon::now()->subSeconds(self::$timeout))->exists()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,9 @@ use Flarum\User\DisplayName\UsernameDriver;
|
||||
use Flarum\User\Event\EmailChangeRequested;
|
||||
use Flarum\User\Event\Registered;
|
||||
use Flarum\User\Event\Saving;
|
||||
use Flarum\User\Throttler\EmailActivationThrottler;
|
||||
use Flarum\User\Throttler\EmailChangeThrottler;
|
||||
use Flarum\User\Throttler\PasswordResetThrottler;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
@ -51,6 +54,14 @@ class UserServiceProvider extends AbstractServiceProvider
|
||||
User::class => [Access\UserPolicy::class],
|
||||
];
|
||||
});
|
||||
|
||||
$this->container->extend('flarum.api.throttlers', function (array $throttlers, Container $container) {
|
||||
$throttlers['emailChangeTimeout'] = $container->make(EmailChangeThrottler::class);
|
||||
$throttlers['emailActivationTimeout'] = $container->make(EmailActivationThrottler::class);
|
||||
$throttlers['passwordResetTimeout'] = $container->make(PasswordResetThrottler::class);
|
||||
|
||||
return $throttlers;
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerDisplayNameDrivers()
|
||||
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\api\users;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\Throttler\EmailActivationThrottler;
|
||||
|
||||
class SendActivationEmailTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
[
|
||||
'id' => 3,
|
||||
'username' => 'normal2',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'normal2@machine.local',
|
||||
'is_email_confirmed' => 0,
|
||||
'last_seen_at' => Carbon::now()->subSecond(),
|
||||
],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function users_can_send_confirmation_emails_in_moderate_intervals()
|
||||
{
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/users/3/send-confirmation', [
|
||||
'authenticatedAs' => 3,
|
||||
])
|
||||
);
|
||||
|
||||
// We don't want to delay tests too long.
|
||||
EmailActivationThrottler::$timeout = 5;
|
||||
sleep(EmailActivationThrottler::$timeout + 1);
|
||||
}
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function users_cant_send_confirmation_emails_too_fast()
|
||||
{
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/users/3/send-confirmation', [
|
||||
'authenticatedAs' => 3,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Tests\integration\api\users;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\Throttler\PasswordResetThrottler;
|
||||
|
||||
class SendPasswordResetEmailTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
[
|
||||
'id' => 3,
|
||||
'username' => 'normal2',
|
||||
'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
|
||||
'email' => 'normal2@machine.local',
|
||||
'is_email_confirmed' => 0,
|
||||
'last_seen_at' => Carbon::now()->subSecond(),
|
||||
],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function users_can_send_password_reset_emails_in_moderate_intervals()
|
||||
{
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/forgot', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'email' => 'normal2@machine.local'
|
||||
]
|
||||
])
|
||||
);
|
||||
|
||||
// We don't want to delay tests too long.
|
||||
PasswordResetThrottler::$timeout = 5;
|
||||
sleep(PasswordResetThrottler::$timeout + 1);
|
||||
}
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function users_cant_send_confirmation_emails_too_fast()
|
||||
{
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$response = $this->send(
|
||||
$this->request('POST', '/api/forgot', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'email' => 'normal2@machine.local'
|
||||
]
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ namespace Flarum\Tests\integration\api\users;
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\Throttler\EmailChangeThrottler;
|
||||
use Flarum\User\User;
|
||||
|
||||
class UpdateTest extends TestCase
|
||||
@ -156,6 +157,62 @@ class UpdateTest extends TestCase
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function users_can_request_email_change_in_moderate_intervals()
|
||||
{
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/users/3', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'email' => 'someOtherEmail@example.com',
|
||||
]
|
||||
],
|
||||
'meta' => [
|
||||
'password' => 'too-obscure'
|
||||
]
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
// We don't want to delay tests too long.
|
||||
EmailChangeThrottler::$timeout = 5;
|
||||
sleep(EmailChangeThrottler::$timeout + 1);
|
||||
}
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function users_cant_request_email_change_too_fast()
|
||||
{
|
||||
for ($i = 0; $i < 2; $i++) {
|
||||
$response = $this->send(
|
||||
$this->request('PATCH', '/api/users/3', [
|
||||
'authenticatedAs' => 3,
|
||||
'json' => [
|
||||
'data' => [
|
||||
'attributes' => [
|
||||
'email' => 'someOtherEmail@example.com',
|
||||
]
|
||||
],
|
||||
'meta' => [
|
||||
'password' => 'too-obscure'
|
||||
]
|
||||
],
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
$this->assertEquals(429, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user