Skip to content
30 changes: 30 additions & 0 deletions config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,34 @@

'middleware' => 'web',

/*
|--------------------------------------------------------------------------
| Auth Route Middleware
|--------------------------------------------------------------------------
|
| Additional middleware applied to the frontend auth routes (login,
| register, password reset, etc). Useful for rate limiting, e.g.
| 'throttle:5,1' to allow 5 attempts per minute.
|
*/

'auth_middleware' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':5,1',
],

/*
|--------------------------------------------------------------------------
| Forms Route Middleware
|--------------------------------------------------------------------------
|
| Additional middleware applied to the frontend form submission route.
| Useful for rate limiting, e.g. 'throttle:10,1' to allow 10 submissions
| per minute.
|
*/

'forms_middleware' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':10,1',
],

];
4 changes: 2 additions & 2 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@

Route::name('statamic.')->group(function () {
Route::group(['prefix' => config('statamic.routes.action')], function () {
Route::post('forms/{form}', [FormController::class, 'submit'])->middleware([HandlePrecognitiveRequests::class])->name('forms.submit');
Route::post('forms/{form}', [FormController::class, 'submit'])->middleware(array_merge([HandlePrecognitiveRequests::class], (array) config('statamic.routes.forms_middleware', [])))->name('forms.submit');

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

Route::group(['prefix' => 'auth', 'middleware' => [AuthGuard::class]], function () {
Route::group(['prefix' => 'auth', 'middleware' => array_merge([AuthGuard::class], (array) config('statamic.routes.auth_middleware', []))], function () {
Route::get('logout', [LoginController::class, 'logout'])->name('logout');

Route::group(['middleware' => [HandlePrecognitiveRequests::class]], function () {
Expand Down
124 changes: 124 additions & 0 deletions tests/Routing/RouteMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

namespace Tests\Routing;

use Illuminate\Routing\Middleware\ThrottleRequests;
use Orchestra\Testbench\Attributes\DefineEnvironment;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Facades\Blueprint;
use Statamic\Facades\Form;
use Tests\PreventSavingStacheItemsToDisk;
use Tests\TestCase;

class RouteMiddlewareTest extends TestCase
{
use PreventSavingStacheItemsToDisk;

protected function withAuthThrottleMiddleware($app)
{
$app['config']->set('statamic.routes.auth_middleware', [ThrottleRequests::class.':2,1']);
}

protected function withFormsThrottleMiddleware($app)
{
$app['config']->set('statamic.routes.forms_middleware', [ThrottleRequests::class.':2,1']);
}

#[Test]
public function no_extra_middleware_is_applied_to_auth_routes_by_default()
{
for ($i = 0; $i < 5; $i++) {
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])
->assertStatus(302);
}
}

#[Test]
#[DefineEnvironment('withAuthThrottleMiddleware')]
public function custom_middleware_is_applied_to_auth_login_route()
{
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(429);
}

#[Test]
#[DefineEnvironment('withAuthThrottleMiddleware')]
public function custom_auth_middleware_is_applied_to_all_auth_routes()
{
$this->post('/!/auth/password/email', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/auth/password/email', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/auth/password/email', ['email' => '[email protected]'])->assertStatus(429);
}

#[Test]
#[DefineEnvironment('withAuthThrottleMiddleware')]
public function custom_auth_middleware_does_not_affect_forms_route()
{
$this->createContactForm();

// Auth routes reach the throttle limit
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(429);

// Forms route is unaffected
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
}

#[Test]
public function no_extra_middleware_is_applied_to_forms_route_by_default()
{
$this->createContactForm();

for ($i = 0; $i < 5; $i++) {
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
}
}

#[Test]
#[DefineEnvironment('withFormsThrottleMiddleware')]
public function custom_middleware_is_applied_to_forms_route()
{
$this->createContactForm();

$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(429);
}

#[Test]
#[DefineEnvironment('withFormsThrottleMiddleware')]
public function custom_forms_middleware_does_not_affect_auth_routes()
{
$this->createContactForm();

// Forms route reaches the throttle limit
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(302);
$this->post('/!/forms/contact', ['email' => '[email protected]'])->assertStatus(429);

// Auth routes are unaffected
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
$this->post('/!/auth/login', ['email' => '[email protected]', 'password' => 'wrong'])->assertStatus(302);
}

private function createContactForm(): void
{
$blueprint = Blueprint::make()->setContents([
'fields' => [
['handle' => 'email', 'field' => ['type' => 'text', 'validate' => 'required|email']],
],
]);

Blueprint::shouldReceive('find')->with('forms.contact')->andReturn($blueprint);
Blueprint::makePartial();

$form = Form::make()->handle('contact');
Form::shouldReceive('find')->with('contact')->andReturn($form);
Form::makePartial();
}
}
Loading