Skip to content

Commit a9edff0

Browse files
jasonvargaclaude
andcommitted
rate limit cp auth routes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dc438ad commit a9edff0

3 files changed

Lines changed: 60 additions & 3 deletions

File tree

routes/cp.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,12 +122,12 @@
122122
Route::group(['prefix' => 'auth'], function () {
123123
if (config('statamic.cp.auth.enabled', true)) {
124124
Route::get('login', [LoginController::class, 'showLoginForm'])->name('login');
125-
Route::post('login', [LoginController::class, 'login']);
125+
Route::post('login', [LoginController::class, 'login'])->middleware('throttle:statamic.cp.auth');
126126

127127
Route::get('password/reset', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
128-
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
128+
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->middleware('throttle:statamic.cp.auth')->name('password.email');
129129
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
130-
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->name('password.reset.action');
130+
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->middleware('throttle:statamic.cp.auth')->name('password.reset.action');
131131

132132
if (TwoFactor::enabled()) {
133133
Route::get('two-factor-challenge', [TwoFactorChallengeController::class, 'index'])->name('two-factor-challenge');

src/Providers/AuthServiceProvider.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ public function boot()
175175
return Limit::perMinute(4)->by($request->ip());
176176
});
177177

178+
RateLimiter::for('statamic.cp.auth', function (Request $request) {
179+
return RateLimiter::limiter('statamic.auth')($request);
180+
});
181+
178182
RateLimiter::for('statamic.forms', function (Request $request) {
179183
return Limit::perMinute(10)->by($request->ip());
180184
});

tests/Feature/RateLimitingTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,36 @@ public function forms_endpoint_is_rate_limited()
5454
$this->post('/!/forms/contact')->assertRateLimited();
5555
}
5656

57+
#[Test]
58+
public function cp_login_endpoint_is_rate_limited()
59+
{
60+
collect(range(1, 4))->each(fn () => $this->post('/cp/auth/login')->assertNotRateLimited());
61+
$this->post('/cp/auth/login')->assertRateLimited();
62+
}
63+
64+
#[Test]
65+
public function cp_password_email_endpoint_is_rate_limited()
66+
{
67+
collect(range(1, 4))->each(fn () => $this->post('/cp/auth/password/email')->assertNotRateLimited());
68+
$this->post('/cp/auth/password/email')->assertRateLimited();
69+
}
70+
71+
#[Test]
72+
public function cp_password_reset_endpoint_is_rate_limited()
73+
{
74+
collect(range(1, 4))->each(fn () => $this->post('/cp/auth/password/reset')->assertNotRateLimited());
75+
$this->post('/cp/auth/password/reset')->assertRateLimited();
76+
}
77+
78+
#[Test]
79+
public function cp_and_frontend_auth_have_independent_buckets()
80+
{
81+
collect(range(1, 4))->each(fn () => $this->post('/!/auth/login')->assertNotRateLimited());
82+
$this->post('/!/auth/login')->assertRateLimited();
83+
84+
$this->post('/cp/auth/login')->assertNotRateLimited();
85+
}
86+
5787
#[Test]
5888
public function auth_rate_limiter_can_be_overridden()
5989
{
@@ -65,6 +95,29 @@ public function auth_rate_limiter_can_be_overridden()
6595
$this->post('/!/auth/login')->assertRateLimited();
6696
}
6797

98+
#[Test]
99+
public function cp_auth_rate_limiter_inherits_overrides_to_statamic_auth()
100+
{
101+
RateLimiter::for('statamic.auth', fn ($request) => Limit::perMinute(2)->by($request->ip()));
102+
103+
$this->post('/cp/auth/login')->assertNotRateLimited();
104+
$this->post('/cp/auth/login')->assertNotRateLimited();
105+
$this->post('/cp/auth/login')->assertRateLimited();
106+
}
107+
108+
#[Test]
109+
public function cp_auth_rate_limiter_can_be_overridden_independently()
110+
{
111+
RateLimiter::for('statamic.cp.auth', fn ($request) => Limit::perMinute(2)->by($request->ip()));
112+
113+
$this->post('/cp/auth/login')->assertNotRateLimited();
114+
$this->post('/cp/auth/login')->assertNotRateLimited();
115+
$this->post('/cp/auth/login')->assertRateLimited();
116+
117+
// Frontend auth still uses the default 4/min
118+
collect(range(1, 4))->each(fn () => $this->post('/!/auth/login')->assertNotRateLimited());
119+
}
120+
68121
#[Test]
69122
public function forms_rate_limiter_can_be_overridden()
70123
{

0 commit comments

Comments
 (0)