Skip to content

Commit dff1d0c

Browse files
committed
change of approach to specify throttle rate limiters
1 parent 9279220 commit dff1d0c

3 files changed

Lines changed: 92 additions & 4 deletions

File tree

routes/web.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,24 @@
3434

3535
Route::name('statamic.')->group(function () {
3636
Route::group(['prefix' => config('statamic.routes.action')], function () {
37-
Route::post('forms/{form}', [FormController::class, 'submit'])->middleware([HandlePrecognitiveRequests::class])->name('forms.submit');
37+
Route::post('forms/{form}', [FormController::class, 'submit'])->middleware([HandlePrecognitiveRequests::class, 'throttle:statamic.forms'])->name('forms.submit');
3838

3939
Route::get('protect/password', [PasswordProtectController::class, 'show'])->name('protect.password.show')->middleware([HandleInertiaRequests::class]);
4040
Route::post('protect/password', [PasswordProtectController::class, 'store'])->name('protect.password.store');
4141

4242
Route::group(['prefix' => 'auth', 'middleware' => [AuthGuard::class]], function () {
4343
Route::get('logout', [LoginController::class, 'logout'])->name('logout');
4444

45-
Route::group(['middleware' => [HandlePrecognitiveRequests::class]], function () {
45+
Route::group(['middleware' => [HandlePrecognitiveRequests::class, 'throttle:statamic.auth']], function () {
4646
Route::post('login', [LoginController::class, 'login'])->name('login');
4747
Route::post('register', RegisterController::class)->name('register');
4848
Route::post('profile', ProfileController::class)->name('profile');
4949
Route::post('password', PasswordController::class)->name('password');
5050
});
5151

52-
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
52+
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->middleware('throttle:statamic.auth')->name('password.email');
5353
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
54-
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->name('password.reset.action');
54+
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->middleware('throttle:statamic.auth')->name('password.reset.action');
5555

5656
Route::group(['prefix' => 'passkeys'], function () {
5757
Route::middleware(ThrottleRequests::class.':30,1')->group(function () {

src/Providers/AuthServiceProvider.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,14 @@ public function boot()
171171
: $broker;
172172
});
173173

174+
RateLimiter::for('statamic.auth', function (Request $request) {
175+
return Limit::perMinute(4)->by($request->ip());
176+
});
177+
178+
RateLimiter::for('statamic.forms', function (Request $request) {
179+
return Limit::perMinute(10)->by($request->ip());
180+
});
181+
174182
RateLimiter::for('two-factor', function (Request $request) {
175183
return Limit::perMinute(5)->by($request->session()->get('login.id'));
176184
});

tests/Feature/RateLimitingTest.php

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use Illuminate\Cache\RateLimiting\Limit;
6+
use Illuminate\Support\Facades\Cache;
7+
use Illuminate\Support\Facades\RateLimiter;
8+
use PHPUnit\Framework\Attributes\Test;
9+
use Tests\TestCase;
10+
11+
class RateLimitingTest extends TestCase
12+
{
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
Cache::flush();
17+
}
18+
19+
private function assertNotRateLimited($response): void
20+
{
21+
$this->assertNotEquals(429, $response->getStatusCode(), 'Request was unexpectedly rate limited.');
22+
}
23+
24+
#[Test]
25+
public function login_endpoint_is_rate_limited()
26+
{
27+
collect(range(1, 4))->each(fn () => $this->assertNotRateLimited($this->post('/!/auth/login')));
28+
$this->post('/!/auth/login')->assertStatus(429);
29+
}
30+
31+
#[Test]
32+
public function register_endpoint_is_rate_limited()
33+
{
34+
collect(range(1, 4))->each(fn () => $this->assertNotRateLimited($this->post('/!/auth/register')));
35+
$this->post('/!/auth/register')->assertStatus(429);
36+
}
37+
38+
#[Test]
39+
public function password_email_endpoint_is_rate_limited()
40+
{
41+
collect(range(1, 4))->each(fn () => $this->assertNotRateLimited($this->post('/!/auth/password/email')));
42+
$this->post('/!/auth/password/email')->assertStatus(429);
43+
}
44+
45+
#[Test]
46+
public function password_reset_endpoint_is_rate_limited()
47+
{
48+
collect(range(1, 4))->each(fn () => $this->assertNotRateLimited($this->post('/!/auth/password/reset')));
49+
$this->post('/!/auth/password/reset')->assertStatus(429);
50+
}
51+
52+
#[Test]
53+
public function forms_endpoint_is_rate_limited()
54+
{
55+
collect(range(1, 10))->each(fn () => $this->assertNotRateLimited($this->post('/!/forms/contact')));
56+
$this->post('/!/forms/contact')->assertStatus(429);
57+
}
58+
59+
#[Test]
60+
public function auth_rate_limiter_can_be_overridden()
61+
{
62+
// Simulate a developer overriding the default 4/min limit to 2/min
63+
RateLimiter::for('statamic.auth', fn ($request) => Limit::perMinute(2)->by($request->ip()));
64+
65+
$this->assertNotRateLimited($this->post('/!/auth/login'));
66+
$this->assertNotRateLimited($this->post('/!/auth/login'));
67+
$this->post('/!/auth/login')->assertStatus(429);
68+
}
69+
70+
#[Test]
71+
public function forms_rate_limiter_can_be_overridden()
72+
{
73+
// Simulate a developer overriding the default 10/min limit to 2/min
74+
RateLimiter::for('statamic.forms', fn ($request) => Limit::perMinute(2)->by($request->ip()));
75+
76+
$this->assertNotRateLimited($this->post('/!/forms/contact'));
77+
$this->assertNotRateLimited($this->post('/!/forms/contact'));
78+
$this->post('/!/forms/contact')->assertStatus(429);
79+
}
80+
}

0 commit comments

Comments
 (0)