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());
Related
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.
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');
});
I am trying to create my own authentication for my API based on external Oauth2 provider. I am using Lumen as my API backend and I have to secure some of my endpoints for users who don't have a valid access token.
So I started as it is written in docs. I have uncommented $app->register(App\Providers\AuthServiceProvider::class); in bootstrap/app.php
I have created specific login in AuthServiceProvider:
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
/**
* Boot the authentication services for the application.
*
* #return void
*/
public function boot()
{
// Here you may define how you wish users to be authenticated for your Lumen
// application. The callback which receives the incoming request instance
// should return either a User instance or null. You're free to obtain
// the User instance via an API token or any other method necessary.
$this->app['auth']->viaRequest('api', function ($request) {
// my logic
});
}
}
And I've also secured my routes in routes/web/php:
$router->group(['prefix' => 'api', 'middleware' => 'auth'], function () use ($router){
$router->get('grant-access', ['uses' => 'DatabaseController#grantAccess']);
$router->get('refresh-access', ['uses' => 'DatabaseController#refreshAccess']);
});
I have investigated that in AuthManager there is a method guard() which causes the problem:
/**
* Attempt to get the guard from the local cache.
*
* #param string|null $name
* #return \Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard
*/
public function guard($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}
Beacuse variable $name is an object and object can't be used as keys in PHP arrays.
So my question have I missed something to switch on Authentication in Lumen?
Okey, It took me few minutes, but I found out that I have to uncomment also this in bootstrap/app.php:
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
]);
I believe that this should be also mentioned in Lumen docs next to that red note.
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.
I'm trying to implement a Token based authentication in Laravel 5.2. What I've done is:
Routes.php
Route::group([
'middleware' => ['auth:api']
], function() {
Route::get('/api/reservation', 'apiController#get');
});
I've modified the User model and added the api_token field and added a new user with a random string via seeders:
Migration
Schema::table('users', function (Blueprint $table) {
$table->string('api_token', 60)->unique()->nullable()->default(null);
});
Seeder
User::create([
...,
'api_token' => str_random(60),
]);
Controller
class apiController extends Controller
{
function get(Request $request) {
return Reserva::all();
}
}
Then, In Postman I try to GET the url adding /api/reservation?api_token=xxxxx with the token I have in the database but I always get Unauthorized. Something weird is if I do a dd($this->auth) on the authentication middleware I get a TokenGuard object with the name='web'. Isn't It supposed to be api?
Maybe I'm misunderstanding something, can you guys give me a hint? Thank you
The auth:api middleware you are using uses Laravel Passport. You cannot use it if you want a custom token based authentication like you do where you create your own tokens.
If you want to use Passport, do this:
Keep your routes like that. The routes that require authentication must be inside of the auth:api middleware.
You can remove the api_token field of your users table. The $table->rememberToken() function in the migration is completely different than the API token you think of. In fact, tokens are not stored in the database at all. The token you see in the oauth_access_token table in the database is not the token you use for you HTTP requests.
Do NOT create a custom token like you do. Check that the login/password couple of the user is valid, generate a token and return it to the consumer of the API like that:
if (Auth::attempt(['login' => $req->login, 'password' => $req->password])) {
$user = Auth::user();
$token = $user->createToken('ToutelaBreizh')->accessToken;
return response()->json(['token' => $token],200);
}
Beware to place the login/register routes OUTSIDE of the auth:api middleware, otherwise you would need to give a token to a route that is supposed to give you this token - it makes no sense.
Next, make sure you send the token to the API in the Authorization header of the request, and not in the request parameters like you do. Add in Postman the following header:
Authorization: Bearer your_token_retrieved_before
Now you're pretty much done, you can use your API with Postman.
In case someone else face this problem. My problem was In the Authenticate middleware. I had an old version somehow.
I had this Authenticate middleware:
class Authenticate
{
/**
* The Guard implementation.
*
* #var Guard
*/
protected $auth;
/**
* Create a new filter instance.
*
* #param Guard $auth
* #return void
*/
public function __construct(Guard $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if ($this->auth->guest()) {
if ($request->ajax() || $request->wantsJson()) {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('login');
}
}
return $next($request);
}
}
And I solved it by changing it with this:
class Authenticate
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest()) {
if ($request->ajax() || $request->wantsJson()) {
return response('Unauthorized.', 401);
} else {
return rediresct()->guest('login');
}
}
return $next($request);
}
}