I am using Fortify (Laravel 8), and it does provide RateLimiter for login and two-factor, but not for the forgot-password requests.
Without a (IP Address) RateLimiter, a very simple bot can execute a huge amount of outgoing emails, basically getting the email service suspended or causing huge costs when using SMTP services that charge per number of emails sent.
I have already tried:
RateLimiter::for('forgot-password', function (Request $request) {
return Limit::perMinute(1)->by($request->ip());
});
In my FortifyServiceProvider.php, but it doesn't work!
There is also no routes in the routes/web.php file to manually apply the throttle middleware.
I've looked at this aswell, and the backend actually throttles too many requests for a specific email. However, that is done through 422 Unprocessable Entity (which is usually a validation error), like so:
{"message":"The given data was invalid.","errors":{"email":["Please wait before retrying."]}}
I've came across your question because I was looking to throttle /fortify/reset-password and /fortify/forgot-password (password.update/password.email) based on the IP only. Both required the user's email address and thus enables an attacker to test existing email addresses, as the backend will happily tell you if it exists or not—without rate limit! The existing rate limit mentioned above (422) will only kick in for multiple requests for one specific email. So this won't help if someone were to enumerate possible emails.
My solution is for reset-password and forgot-password (and rather a workaround). I will omit the reset-password as it is equal to forgot-password.
However, this requires you to overwrite fortify's published route in your web.php. No changes are made to any vendor files. You will have to ensure that after upgrading fortify that the route registration is still equal to the original (https://github.com/laravel/fortify/blob/master/routes/routes.php)
app/Providers/FortifyServiceProvider.php]This will allow 10 requests per minute per IP
public function boot()
{
// ...
RateLimiter::for('forgot-password', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
}
config/fortify.php] 'limiters' => [
// ...
'forgot-password' => 'forgot-password',
],
config/app.php]'providers' => [
// ...
/*
* Package Service Providers...
*/
App\Providers\FortifyServiceProvider::class,
/*
* Application Service Providers...
*/
// ...
App\Providers\RouteServiceProvider::class,
],
routes/web.php]use Laravel\Fortify\Http\Controllers\PasswordResetLinkController;
//...
$limiter = config('fortify.limiters.forgot-password');
// Copied from
// https://github.com/laravel/fortify/blob/c64f1e8263417179d06fd986ef8d716d4c5689e2/routes/routes.php#L55
// with addition of the throttle middleware.
// TODO: Keep this updated when updating fortify!
Route::post(config('fortify.prefix', 'fortify') . '/forgot-password', [PasswordResetLinkController::class, 'store'])
->middleware(['guest', 'throttle:' . $limiter])
->name('password.email');
More than 10 requests from one IP will now throw a 429 Too Many Requests from you backend to the client.
The nice thing about this is that you only overwrite the route-registration but can still use all fortify features as it uses the stock controller.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With