Laravel throttle rate limiter limites access too early - php

I'm working with Laravel 5.8 and I wanted to apply a Rate Limiter that limits the sending request to 500 per minute.
So I tried adding this throttle to the route group:
Route::middleware('throttle:500,1')->group(function () {
...
});
So this means that limits access to the routes after 500 requests in 1 minute.
Now the problem is I get 429 | Too Many Requests too soon!
I mean, it does not seem to be sending 500 requests in a minute but it limits the access somehow.
So what's going wrong here? Why I get Too Many Requests message too early?

The throttle middleware requests are calculated together
if you have another throttle group, these calculated together and may that group consume your limits
Route::get('example1', function () {
return 'ans1';
})->middleware('throttle:5,1');
Route::get('example2', function () {
return 'ans2';
})->middleware('throttle:5,1');
In above case, you have 2 route but if you send 2 request to example1 and 3 request to example2, your rate limit will finish for both of them and you got 429 | Too Many Requests error.

If you're using multiple throttle middlewares in your group and it's nested routes like this:
Route::middleware('throttle:500,1')->group(function () {
Route::post('/foo', 'bar#foo')->middleware('throttle:5,60');
});
the throttle middleware counts each requests twice. for solving this issue,
you must make sure that each route has only one throttle middleware.
But sometimes even though the middleware has been used only once, the request count is still wrong. in that cases, you can make your own customized throttle middleware that takes an extra argument for naming throttle key and prevent re-counting requests (based on this).
To do this, you must first create a middleware (according to this):
php artisan make:middleware CustomThrottleMiddleware
and then, replace the contents with:
<?php
namespace App\Http\Middleware;
use Closure;
use RuntimeException;
use Illuminate\Routing\Middleware\ThrottleRequests;
class CustomThrottleMiddleware extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param int|string $maxAttempts
* #param float|int $decayMinutes
* #param string $keyAppend
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes = 1, $keyAppend = '')
{
$key = $this->resolveRequestSignature($request, $keyAppend);
$maxAttempts = $this->resolveMaxAttempts($request, $maxAttempts);
if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
throw $this->buildException($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes * 60);
$response = $next($request);
return $this->addHeaders(
$response, $maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
/**
* Resolve request signature.
*
* #param \Illuminate\Http\Request $request
* #param string $keyAppend
* #return string
*
* #throws \RuntimeException
*/
protected function resolveRequestSignature($request, $keyAppend='')
{
if ($user = $request->user()) {
return sha1($user->getAuthIdentifier().$keyAppend);
}
if ($route = $request->route()) {
return sha1($route->getDomain().'|'.$request->ip().$keyAppend);
}
throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
}
}
after doing that, you must to register the middleware by updating the app/Http/Kernel.php file:
protected $routeMiddleware = [
//...
'custom_throttle' => \App\Http\Middleware\CustomThrottleMiddleware::class,
];
now you can use this new customized throttle middleware in your routes, something like this:
Route::middleware('throttle:500,1')->group(function () {
Route::post('/foo', 'bar#foo')->middleware('custom_throttle:5,60,foo');
});

Related

Rate Limit for only success requests (Laravel 9)

Is there anyway to apply rate limiting to the route but for only success responses. Like for example if user sends request to send/code endpoint 5 times and if all of them was successful then block the user to send request again. But if 2 of them was unsuccessful (like validation error or something) but 3 was successful then user should have 2 more attempts for the given time.
I know rate limiting checks before request get executed, then block or let the user to continue. But is there anyway to apply my logic or should I try to approach differently?
You would probably need to make your own middleware, but you can extend the ThrottleRequests class and just customize how you want to handle responses:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Arr;
class ThrottleSuccess extends ThrottleRequests
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param array $limits
* #return \Symfony\Component\HttpFoundation\Response
*
* #throws \Illuminate\Http\Exceptions\ThrottleRequestsException
*/
protected function handleRequest($request, Closure $next, array $limits)
{
$response = $next($request); // call the controller first
if ($response->statusCode === 200) { // only hit limiter on successful response
foreach ($limits as $limit) {
if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
$this->limiter->hit($limit->key, $limit->decayMinutes * 60);
}
}
foreach ($limits as $limit) {
$response = $this->addHeaders(
$response,
$limit->maxAttempts,
$this->calculateRemainingAttempts($limit->key, $limit->maxAttempts)
);
}
return $response;
}
}
Then add your middleware to Kernel.php:
protected $routeMiddleware = [
// ...
'throttle.success' => ThrottleSuccess::class,
// ...
];
Then use it in a route like the original throttle middleware:
Route::middleware('throttle.success:5,1')->group(function () {
// ...
});
Note: you may have to override handleRequestUsingNamedLimiter if you want to return a custom response built from RateLimiter::for, I have not done anything for that here.

Throttle issue with server accessing a Laravel API

I have an API that is using Laravel that is being called from another instance of Laravel with Guzzle.
The second server's IP address is triggering the throttle on the API.
I would like to pass through the user's domain and IP address from the second server to the API. I am hoping not to recode the Throttle middleware.
I am wondering if anyone has faced this before and if so how they solved it.
The middleware group on the API is set up like this
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'api' => [
'throttle:60,1',
\Barryvdh\Cors\HandleCors::class,
'bindings',
],
];
relevant throttle code
/**
* Resolve request signature.
*
* #param \Illuminate\Http\Request $request
* #return string
*
* #throws \RuntimeException
*/
protected function resolveRequestSignature($request)
{
if ($user = $request->user()) {
return sha1($user->getAuthIdentifier());
}
if ($route = $request->route()) {
return sha1($route->getDomain().'|'.$request->ip());
}
throw new RuntimeException('Unable to generate the request signature. Route unavailable.');
}
You can pass the client's IP address with the X_FORWARDED_FOR header, that way the IP address of the second server is not blocked.
Route::get('/', function (Request $request) {
$client = new \GuzzleHttp\Client();
$request = $client->request('GET', '/api/example', [
'headers' => ['X_FORWARDED_FOR' => $request->ip()]
]);
$response = $request->getBody();
});
On your main server you need to add your second server as a trusted proxy (docs) to App\Http\Middleware\TrustProxies in order to take the IP from this header.
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* #var array
*/
protected $proxies = [
'192.168.1.1', // <-- set the ip of the second server here
];
//...
}
Now every call to $request->ip() on the main server will have the original client IP instead of the second server's IP. That will also affect the throttling.
The out of the box solution, if you are using a version >= 5.6, is to use the dynamic rate limit.
Dynamic Rate Limiting
You may specify a dynamic request maximum based on an attribute of the authenticated User model. For example, if your User model contains a rate_limit attribute, you may pass the name of the attribute to the throttle middleware so that it is used to calculate the maximum request count:
Route::middleware('auth:api', 'throttle:rate_limit,1')->group(function () {
Route::get('/user', function () {
//
});
});
The relevant part of the code
/**
* Resolve the number of attempts if the user is authenticated or not.
*
* #param \Illuminate\Http\Request $request
* #param int|string $maxAttempts
* #return int
*/
protected function resolveMaxAttempts($request, $maxAttempts)
{
if (Str::contains($maxAttempts, '|')) {
$maxAttempts = explode('|', $maxAttempts, 2)[$request->user() ? 1 : 0];
}
if (! is_numeric($maxAttempts) && $request->user()) {
$maxAttempts = $request->user()->{$maxAttempts};
}
return (int) $maxAttempts;
}
Thus, you could add a rate_limit property in the user (representing the second server) and pass a bigger number
EDIT:
If you don't want to have the caller authenticated, you can easily overwrite the resolveMaxAttempts method to calculate the limit dynamically based on the request data (you could use any parameter, the host, the ip, etc):
protected function resolveMaxAttempts($request, $maxAttempts)
{
if (in_array(request->ip(), config('app.bypassThrottleMiddleware')) {
return PHP_INT_MAX;
}
return parent::resolveMaxAttempts($request, $maxAttempts);
}
and in your config/app.php add:
'bypassThrottleMiddleware' => ['0.0.0.0'],
if ($route = $request->route()) {
return sha1($route->getDomain().'|'.$request->ip());

Getting a redirect loop when using Middleware

In my Laravel application I use the auth Middleware to ensure only authenticated users can reach particular routes.
So, in my routes/web.php I have something like this:
Route::group(['middleware' => ['auth', 'user.required.fields']], function () {
}
As you can see I've added an extra Middleware: user.required.fields it looks like this
<?php
namespace App\Http\Middleware;
use Closure;
class CheckUserRequiredFields
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(auth()->user()->has_filled_required_fields){
return $next($request);
}
else{
return redirect()->route('new-user');
}
}
}
The attribute in question looks like this:
/**
* If the user has filled in their role, department and location, allow full access to the intranet
*
* #return void
*/
public function getHasFilledRequiredFieldsAttribute()
{
if ($this->role && $this->department && $this->location) {
return true;
} else {
return false;
}
}
However, this causes an infinite loop.
Is there something in the auth middleware that would cause this? It's almost like the middleware calls itself over and over.
The code will correctly redirect to route('new-user)` but then repeatedly hits the route.
I think you have your route('new-user') defined within this group:
Route::group(['middleware' => ['auth', 'user.required.fields']], function () {
//
}
So, it will be checked by the middleware, that returns the same route again and again, which cause the mentioned loop.
A possible solution is to remove the route from this grop and also not protect it with the CheckUserRequiredFields middleware.

Laravel cannot pass multiple middleware

recently i created two middlewares which is one for user called device, and one other for super user which is high level of admin. This is my middleware
Role Device Middleware
<?php
namespace App\Http\Middleware;
use Closure;
class RoleDevice
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(Auth::check() && Auth::User()->role=='device'){
return $next($request);
}
return redirect()->route('login')->with('danger',"You don't have an access");
}
}
Role Device Super User
<?php
namespace App\Http\Middleware;
use Closure;
use Auth;
use User;
class RoleSuper
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(Auth::check() && Auth::User()->role=='super'){
return $next($request);
}
return redirect()->route('login')->with('danger',"You don't have an access");
}
}
after i created the middlewares, i put into the routes which is one route could access two middlewares. Here is one of my route.
Route::get('/dashboard','DashboardController#index')->middleware(['rolesuper','roledevice'])->name('dashboard');
and when i try to log in into my website, it returns
You don't have an access
which is don't pass into the middleware.
i hope i get any comments above !
thanks.
Middlewares are executed in the order the are passed. So in case first middleware returns redirect response that's it - second middleware won't be executed.
You could combine both middleware into one and pass available roles as middleware parameter or just create single middleware for this that will verify if user is authorized.

Laravel 5.1 ACL

I've read about the new policy features in Laravel 5.1.
It looks from the docs that a blacklist approach is chosen by default. E.g. a controller actions is possible until access is checked and denied using a policy.
Is it possible to turn this into a whitelist approach? Thus, every controller action is denied except when it's explicitly granted.
I just found a rather clean way I think, in your routes, you pass a middleware and the policy that needs to be checked.
Example code:
<?php
namespace App\Http\Middleware;
use Closure;
class PolicyMiddleware
{
/**
* Run the request filter.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string $policy The policy that will be checked
* #return mixed
*/
public function handle($request, Closure $next, $policy)
{
if (! $request->user()->can($policy)) {
// Redirect...
}
return $next($request);
}
}
And the corresponding route:
Route::put('post/{id}', ['middleware' => 'policy:policytobechecked', function ($id) {
//
}]);

Categories