When attempting to implement throttling on an API endpoint, attempting to set different throttle controls for unauthenticated vs authenticated requests results in unexpected behaviour whereby the throttle middleware will always use the "unauthenticated" rate.
Throttling for authenticated vs unauthenticated requests was added in 5.5 in https://github.com/laravel/framework/pull/19807
This appears due to a race condition, whereby the throttle is run either before or after the token authentication requirements.
Set middleware group to throttle api requests
$middlewareGroups = [
'api' => [
// allow 10 requests per minute for unauthenticated requests
// allow 60 request per minute for authenticated requests
'throttle:10|60,1',
'bindings',
],
];
Set a route to require authentication
Route::get('/', 'IndexController')
->middleware(['auth:api', 'api'])
Attempting to hit the API endpoint results in the throttle requests middleware to always return the unauthenticated request limit, which is based on the request signature of an un-authenticated request.
This appears due to "request->user()" (https://github.com/laravel/framework/blob/bd352a0d2ca93775fce8ef02365b03fc4fb8cbb0/src/Illuminate/Routing/Middleware/ThrottleRequests.php#L75) not yet being set, due to authentication occuring after throttle processing.
Additionally, attempting to move the authentication to occur before the throttle results in a HTTP 401 response being created, but this will never trigger the throttle actions, as the 401 response is returned before hitting other middleware functionality.
Using the middleware auth:api means you're using the middleware auth with the parameter (in this case guard name) api. The api guard uses token to authenticate, and you're not passing any. You're never using the throttle middleware in your example.
@sisve / @GrahamCampbell — confirming that an api token is included in the request — additionally, using auth:api is expected, as this is an api endpoint where authentication is completed via the the api guard.
The issue relates to the auth guard throwing an authentications exception, with an HTTP 401 "Unauthorized" response error being returned.
The exception behaviour stops request processing, including the throttle middleware. This prevents the throttle middleware from being considered in the request, resulting in only authenticated requests being throttled.
Additionally, changing the ordering of the middleware to use the throttle middleware prior to the auth guard will result in the throttling middleware logic being based on an unauthenticated request, as the auth middleware has not run yet.
The problem itself is more-so a race condition, whereby we want to consider if the request is authenticated to determine the throttle limits, but authentication needs to occur prior to this and the guard stops processing when a failed authentication occurs.
Also — the api middleware is added to the api routes by default...
@GrahamCampbell - can we get this re-opened please?
FYI — Our current workaround has been to create a new authentication "attempt" middleware that extends https://github.com/laravel/framework/blob/5.5/src/Illuminate/Auth/Middleware/Authenticate.php
removing the exception from being thrown in the "attempt" middleware class
This essentially turns the middleware into an authentication attempt, with the existing framework provided middleware responsible for throwing the exception.
There's likely a cleaner / better way to approach this - the key issue being that we need an auth attempt to occur prior to the throttle logic, to ensure the throttle is based on whether the request was authenticated vs unauthenticated.
At the same time, we want the authentication middleware to throw the exception in most cases - as we rely on this to prevent unauthenticated access..
Kernel.php
protected $middlewareGroups = [
....
'api' => [
'auth.attempt:api',
'throttle:10|60,1',
'auth:api',
'bindings',
],
....
];
...
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.attempt' => \App\Http\Middleware\Authenticate::class,
...
];
Is there any solution to this other than implementing the above workaround? I'm seeing exactly the same issue on Laravel 5.8.
Any news on this?
Any chance we could reopen this? This is still an issue in Laravel 7.15.0.
I had the same problem and was because I was sending an Authorization: Bearer null header from the client.
Most helpful comment
Is there any solution to this other than implementing the above workaround? I'm seeing exactly the same issue on Laravel 5.8.