Rate Limit for only success requests (Laravel 9) - php

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.

Related

Laravel throttle rate limiter limites access too early

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');
});

What is the opposite of middleware in Laravel?

Middleware in Laravel can be used to add app wide logic (or specific to specific routes or group of routes) before any application/business logic is applied. I want to do the same, but after all the application/business logic is done. What I love about middleware is that it centralizes the place where said logic is applied. Is there a way to do that at the end of the request/response lifecycle?
One option is using transformers, but I don't find it as clean as middleware for some reason (maybe because it's done by a third party?)
sample use case: I want to have a group of endpoints always return values in an alternate currency rather than USD only when such requests are made from a certain type of shoppers from a certain geographical area (which I already know). So I will need to perform business logic, and then right before I send the json response back, I want to "hijack" said response and replace all USD values with another currency of my choosing.
Ideas? (I'm using Laravel 5.5)
You can do it in.... the middleware right after the call for the closure
/**
* #param Request $request
* #param \Closure $next
* #return Response
*/
public function handle(Request $request, \Closure $next)
{
//middleware logic
$response = $next($request); //dont return it
//post application/business logic here
return $response;
}
Edit from #ceejayoz comment:
You can use terminate() method in your middleware class to run code After that the response has been sent to the client with the condition that your web server is using FastCGI.
public function handle(Request $request, \Closure $next)
{
//middleware logic
return $next($request); //dont return it
}
public function terminate($request, $response)
{
//post application/business logic here
//no need for return instruction
}
Edit 2
For transformation of the json output, the best solution would be to use the ResourceCollection.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class Product extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
$multiplier = $isUs?1.2:1;
return [
'price' = $this->price * $multiplier,
];
}
}
In your controller use this class as the response for the controller
public function show($productId)
{
return new \App\Http\Resources\Product(Product::find($productId));
}

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.

Ajax Auth redirect on Laravel 5.6

I have a Like button that fires an Ajax Post, this route is protected by auth:api middleware:
myproject/routes/api.php
Route::group(['middleware' => ['auth:api']], function () {
Route::post('/v1/like', 'APIController#set_like');
});
When an authenticated user clicks the like button, no problem at all, everything works smoothly. But when guests click the button, I redirected them to login page via Javascript and after authentication they are redirected to the page specified in RedirectIfAuthenticated middleware, so usually to /home.
I modified that middleware as follows:
myproject/app/Http/Middleware/RedirectIfAuthenticated.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect()->intended('/home');
}
return $next($request);
}
}
My Ajax call is this:
var toggleLike = function(){
var token = USER_TOKEN; //javascript variable
var current_type = $(thisLikeable).attr("data-type");
var current_id = $(thisLikeable).attr("data-id");
var jqxhr = $.post( APP_URL + "/api/v1/like", {api_token: token, type: current_type, id: current_id}, function(data) {
setLikeAppearance(data.message);
})
.fail(function(xhr, status, error){
if (xhr.status == 401) {
window.location = APP_URL + "/login" ;
}
});
};
The problem here's the intended() function, which for Ajax calls is not storing the correct session variable and I am not figuring out how to set it properly.
I am clearly missing something obvious, can anyone help?
Cheers!
EDIT
What I want to achieve is this:
GUEST is in //mysite/blabla
clicks Like button
gets redirected to login
logs in (or register)
gets redirected to //mysite/blabla with the Like already triggered on
What's happening is that in APIs sessions are not managed or in other words it's stateless. So the session middleware is not implemented on Laravel framework for API requests. Though you can manually add, it's not idle to use. So if the API does not use sessions and uses the redirect, fronted does not know about it, as API and frontend work as two separate apps. SO you need to send the frontend the status of the response and let the frontend handle the redirect as you have done with ajax. Just remove the redirect if unauthenticated and let the API throw unauthorized exception. Then, from the handler, handle the unauthorized exception.
Here is how to do it.
Add this to app/Exceptions/Handler.php
/**
* Convert an authentication exception into an unauthenticated response.
*
* #param \Illuminate\Http\Request $request
* #param \Illuminate\Auth\AuthenticationException $exception
* #return \Illuminate\Http\Response
*/
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
this will send the user a 401 with message Unauthenticated if the request was json(api request), else(if web request) will redirect to login
check the render method below or check it from source to understand what's happening. when an unauthorized exception is thrown we are telling to check the request type and if it's from an API request, we are sending a json response with 401 status code. So know from frontend we could redirect the user to login page after seeing the 401 status code.
From source
/**
* Render an exception into a response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Symfony\Component\HttpFoundation\Response
*/
public function render($request, Exception $e)
{
if (method_exists($e, 'render') && $response = $e->render($request)) {
return Router::toResponse($request, $response);
} elseif ($e instanceof Responsable) {
return $e->toResponse($request);
}
$e = $this->prepareException($e);
if ($e instanceof HttpResponseException) {
return $e->getResponse();
} elseif ($e instanceof AuthenticationException) {
return $this->unauthenticated($request, $e);
} elseif ($e instanceof ValidationException) {
return $this->convertValidationExceptionToResponse($e, $request);
}
return $request->expectsJson()
? $this->prepareJsonResponse($request, $e)
: $this->prepareResponse($request, $e);
}
AFTER EDIT
The intended method() is only for web routes as it uses session to extract the intended route or manually passed value. Here is the intended() method.
/**
* Create a new redirect response to the previously intended location.
*
* #param string $default
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function intended($default = '/', $status = 302, $headers = [], $secure = null)
{
$path = $this->session->pull('url.intended', $default);
return $this->to($path, $status, $headers, $secure);
}
To achive the redirect to the page the user is comming from you can
1 - Manually pass some queries with url like
/login?redirect=like(not the url, just a mapping for /comments/like url)&value=true(true is liked, false is unliked)
and handle it manually.
2 - Get and check the query parameters from url
3 - Use to() method to pass the intended url instead of using intended(). here is the to() method. (see 4 to see the recommended way)
/**
* Create a new redirect response to the given path.
*
* #param string $path
* #param int $status
* #param array $headers
* #param bool $secure
* #return \Illuminate\Http\RedirectResponse
*/
public function to($path, $status = 302, $headers = [], $secure = null)
{
return $this->createRedirect($this->generator->to($path, [], $secure), $status, $headers);
}
4 - But, I would recommend sending redirect url (I mean the mapping ex: like) as a response to frontend and let the frontend handle the redirecting. As API redirecting will work if the API is used by websites only. Suppose if you are using this same api for a mobile app, wonder how API redirect will work. It's not a work of API to redirect, unless if it's for things like OAuth Authentication, which would have a redirect url specified.
Remember to sanitize the url params to block XSS like stuff. Better
Send some values and map it to the urls. Like
[
//like is mapping
//comments/like is the actual url
'like' => 'comments/like'
]
Then, get the mapping url from array or use frontend mappings.
You can make changes in your RedirectIfAuthenticated.php to distinguish between Ajax call & normal login like this:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string|null $guard
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
if ($request->has('api_token')) {
// do whatever you want
return response()->json('some response');
}
return redirect()->intended('/home');
}
return $next($request);
}
}
Update:
Another solution is to remove Auth middleware from your route. In APIController#set_like function manually login user, trigger like & return json response.
If a button is not for a guest, then you shouldn't render it on page.
Instead, you should render a link to login, then if the user logs in you will redirect him back to where he was before. Now, user can see and click the button.

How to get variable from middleware?

I've created my own middleware for API. Following is my code for getting valid User details based on the request params access_token
namespace App\Http\Middleware;
use Closure;
use App\Exceptions\GeneralException;
use App\Models\Access\User\User;
class authNS
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
try {
$user = User::where("access_token",$request->header('access-token'))->with('details')->first();
} catch (Exception $e) {
return response()->json(['error'=>'Something is wrong']);
}
return $next($request);
}
}
But how can I access this $user variable within my Controller?
You can use onceUsingId() to log a user into the application for a single request. No sessions or cookies will be utilized, which means this method may be helpful when building a stateless API:
So in your middleware you can use it as:
$user = User::where("access_token",$request->header('access-token'))->first();
if($user) {
auth()->onceUsingId($user->id);
}
Then in your controller, you can use it as:
auth()->user()
Docs

Categories