Skip to content
Advertisement

How to RateLimit forgot-password requests in Laravel Fortify?

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.

Advertisement

Answer

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)

  1. Let’s add a new rate limiter [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());
        });
    }
  1. Add it to your fortify config [config/fortify.php]
 'limiters' => [
        // ...
        'forgot-password' => 'forgot-password',
    ],
  1. Make sure that your fortify service provider is called before your own route service provider (which is the case when installed via official Laravel docs) [config/app.php]
'providers' => [

        // ...
        /*
         * Package Service Providers...
         */
        AppProvidersFortifyServiceProvider::class,


        /*
         * Application Service Providers...
         */

        // ...
        AppProvidersRouteServiceProvider::class,

    ],
  1. You can now overwrite the route and attach the rate limiter [routes/web.php]
use LaravelFortifyHttpControllersPasswordResetLinkController;

//...

$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.

User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement