Skip to content

Instantly share code, notes, and snippets.

@stevebauman
Last active July 3, 2024 12:44
Show Gist options
  • Save stevebauman/237b943bba644a698712d53e0b29eae4 to your computer and use it in GitHub Desktop.
Save stevebauman/237b943bba644a698712d53e0b29eae4 to your computer and use it in GitHub Desktop.
Laravel Throttle Validation Rule

Description

Due to the default throttle middleware that will return its own response denying any access to the route you provide it after a certain number of requests, this is a throttle validation rule allowing the throttling of form requests so you're able to restrict only the submission of forms for the specific field of your choice.

For example, maybe a contact form with an email address can only be filled out once every 10 minutes per user.

Here is how you would perform this:

class ContactRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'email' => [
                'required',
                'email',
                new Throttle('contact-form', $maxAttempts = 1, $minutes = 10),
            ],
            'message' => [
                'required',
                'min:10',
                'max:250',
            ],
        ];
    }

Once the user has tried more than once in 10 minutes, a validation error of "Too many attempts. Please try again later." will be returned to the form under the email key.

<?php
namespace App\Rules;
use Illuminate\Http\Request;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Validation\Rule;
class Throttle implements Rule
{
/**
* The throttle key.
*
* @var string
*/
protected $key = 'validation';
/**
* The maximum number of attempts a user can perform.
*
* @var int
*/
protected $maxAttempts = 5;
/**
* The amount of minutes to restrict the requests by.
*
* @var int
*/
protected $decayInMinutes = 10;
/**
* Create a new rule instance.
*
* @param string $key
* @param int $maxAttempts
* @param int $decayInMinutes
*
* @return void
*/
public function __construct($key = 'validation', $maxAttempts = 5, $decayInMinutes = 10)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decayInMinutes = $decayInMinutes;
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
*
* @return bool
*/
public function passes($attribute, $value)
{
if ($this->hasTooManyAttempts()) {
return false;
}
$this->incrementAttempts();
return true;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return __('Too many attempts. Please try again later.');
}
/**
* Determine if the user has too many failed attempts.
*
* @return bool
*/
protected function hasTooManyAttempts()
{
return $this->limiter()->tooManyAttempts(
$this->throttleKey(), $this->maxAttempts
);
}
/**
* Increment the attempts for the user.
*
* @return void
*/
protected function incrementAttempts()
{
$this->limiter()->hit(
$this->throttleKey(), $this->decayInMinutes * 60
);
}
/**
* Get the throttle key for the given request.
*
* @return string
*/
protected function throttleKey()
{
return $this->key . '|' . $this->request()->ip();
}
/**
* Get the rate limiter instance.
*
* @return \Illuminate\Cache\RateLimiter
*/
protected function limiter()
{
return app(RateLimiter::class);
}
/**
* Get the current HTTP request.
*
* @return \Illuminate\Http\Request
*/
protected function request()
{
return app(Request::class);
}
}
@tumainimosha
Copy link

Note for users of recent Laravel version, the cache ttl unit is no longer minutes but seconds.

Thus the $decayInMinutes field is actually seconds, thus value you supply need to be multiplied by 60 to be in minutes

@stevebauman
Copy link
Author

Thanks @tumainimosha! I've just fixed this.

@chrispage1
Copy link

@stevebauman - thanks for sharing this! Very handy for a heavily abused competition form!

@stevebauman
Copy link
Author

Awesome @chrispage1! I'm glad you've found this useful 😄

@y-eight
Copy link

y-eight commented Nov 18, 2020

But @stevebauman if the user did not pass the message validation rules because eg. the text is too short he should get another attempt, right?
In this case, he would not be able to try again with the correct message because of the email throttle.
Is there a chance to implement this issue too, at the moment I have no idea?

@stevebauman
Copy link
Author

Hi @y-eight, you can then use the bail validation rule, which will prevent subsequent rules from executing when the first rule fails.

This prevents the throttle from being hit until all rules pass.

https://laravel.com/docs/8.x/validation#rule-bail

return [
   'email' => [
        'bail',
        'required',
        'email',
        new Throttle('contact-form', $maxAttempts = 1, $minutes = 10),
    ],
];

@y-eight
Copy link

y-eight commented Nov 19, 2020

First thx but I think you are just partly true @stevebauman...
This will work for one validation field (in your example 'email'). Imagine I would add another field, like 'message'.
The user enters a valid email -> the throttle will be registered even if the message does not pass the validator.

@stevebauman
Copy link
Author

stevebauman commented Feb 2, 2022

Yup that's correct @y-eight 👍 If you need this functionality you'll have to add an after validation hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment