Laravel 8 Fortify login says 429 TOO MANY REQUESTS - laravel

I'm having a problem related to 429 TOO MANY REQUESTS. I have used Laravel fortify and my web route is like
Route::get('/', function () {
return view('welcome');
});
Route::get('/dashboard','DashboardController#dashboardView')
->name('dashboard')->middleware('auth');
The problem is coming intermittently, after successful login I redirected to dashboard if immediately I hit logout and instantly try to log-in it is giving 429 TOO MANY REQUESTS and in the address bar the URL is http://127.0.0.1:8000/login. Now if I wait there for a sec and refresh the page it redirects to the dashboard page.
I have searched the web, everyone is saying about throttle and I'm not seeing this as the solution. Please help me.
Thanks.

I stumbled upon the same problem today and did some debugging. When registering the /login route, Fortify applies the Illuminate\Routing\Middleware\ThrottleRequests:login middleware to it. This means, for every request to that route, the ThrottleRequests middleware will call the RateLimiter instance for that specified key. Apparently, Fortify doesn't register a RateLimiter for the login key.
Due to the missing key in the $limiters property of the RateLimiter instance, the ThrottleRequests middleware uses its default fallback, which doesn't handle the edge case "there SHOULD be a rate limiter for that key, but there isn't." really well. The $maxAttempts variable is set to 0 and will result in flaky rate limiting behaviour.
I feel like this is a bug in Fortify, because rate limiting is also happening in the \Laravel\Fortify\Actions\EnsureLoginIsNotThrottled action, which is invoked in the \Laravel\Fortify\Http\Controllers\AuthenticatedSessionController controller. I didn't check this on a fresh Laravel installation, though, so I don't want to jump to conclusions here.
Anyway, long story short: As a workaround, you can simply register a rate limiter for the "login" key in some of your providers, e. g. AppServiceProvider or AuthServiceProvider:
public function boot()
{
RateLimiter::for("login", function () {
Limit::perMinute(5);
});
}
Edit:
I just realized that the rate limiter for the "login" key is indeed provided by Fortify within the FortifyServiceProvider class. If you happen to have a problem similar to the one discussed above, make sure that you added the FortifyServiceProvider class to your providers array in the config/app.php.

I tried everything including the Best Answer but it just didn't work.
Therefore, not even changing the RateLimiter in the FortifyServiceProvider class.
I'd try to log in and get a 429 error after just one login attempt.
Here what was the issue for me, it was the the config/fortify.php file.
I had to change:
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],
to
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 5,
'two-factor' => 5,
],
And funny enough the problem is inherent in the Fortify package itself when you run:
php artisan vendor:publish --provider="Laravel\Fortify\FortifyServiceProvider" as per their documentation instructions.
This fundamental reason being that code within vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php is not able to correctly parse the limit:
/**
* 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;
}
, which means, 'login' is just parsed as 0 and that's what it returns.
Now I don't have to run php artisan cache:clear just to test.

go to
app/http/kernel.php
and remove from routeMiddleware list the line
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,

Just run
php artisan cache:clear

Related

How to use laravel's can() on routes when using JetStream

I'm trying to work out how to use ->can() on a route when combined with JetStream's permissions.
I can see how to create permissions in the JetStreamServiceProvider.php:
protected function configurePermissions()
{
Jetstream::defaultApiTokenPermissions(['read']);
Jetstream::role('admin', __('Administrator'), [
'export:orders'
])->description(__('Administrator users can perform any action.'));
However I can work out how to combine this with a route:
Route::get('/generate-export/orders', [OrderExportController::class,'show'])->name('exportOrders.show')->can(' -- not sure what to put here --');
I've tried a few things and got nowhere.
Those tokens are used for requests authenticated with sanctum. As their documentation states in:
Sanctum 9.x - Token Ability Middleware
Yoiu need to first add 2 entries to the route middleware array in your App\Http\Kernel.php file.
protected $routeMiddleware = [
...,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
And then, you can use those middlewares in your routes.
Route::get('/generate-export/orders', [OrderExportController::class,'show'])
->name('exportOrders.show')
->middleware(['auth:sanctum', 'ability:export:orders']); // check if user has export:orders ability
As for the difference between abilities and ability, to quote the documentation:
The abilities middleware may be assigned to a route to verify that the incoming request's token has all of the listed abilities:
Route::get('/orders', function () {
// Token has both "check-status" and "place-orders" abilities...
})->middleware(['auth:sanctum', 'abilities:check-status,place-orders']);
The ability middleware may be assigned to a route to verify that the incoming request's token has at least one of the listed abilities:
Route::get('/orders', function () {
// Token has the "check-status" or "place-orders" ability...
})->middleware(['auth:sanctum', 'ability:check-status,place-orders']);

Laravel policy always returns 403

so I made a policy and whatever I do the Web page returns 403. im very new to laravel so most likely im missing something :)
I made a model by
php artisan make:model exercise | (I know I'm supposed to capitalize models but it was a typo)
Controller:
php artisan make:controller ExercisesController
Policy:
php artisan make:policy ExercisePolicy -m exercise
I registered policy in AuthServiceProvider.php (Also tried as 'App\Models\exercise'=>'App\Policies\ExercisePolicy'):
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
exercise::class => ExercisePolicy::class,
];
In ExercisesController.php this is the function in which I use authentication:
public function create(\App\Models\User $user)
{
$this->authorize('create', $user);
return view('exercises/create');
}
And in policy this is how my create function looks like
public function create(User $user)
{
return $user->admin == true;
}
The route:
Route::get('/exercises/create', [App\Http\Controllers\ExercisesController::class, 'create']);
I tried putting die("Policy is called); and trying to just return true from the policy create function to check if it gets to that but it still returned 403 , at this point I'm pretty sure that the policy itself is not being called as it also returns 403 on default
If anyone could help thanks in advance!
The call to authorize is using the second argument to figure out what Policy to use. Since the second argument is $user it would be looking for a Policy for the User model which you have not defined. To use the Policy for the exercise model you would have to pass the class name so it would know what Policy to use:
$this->authorize('create', exercise::class);
Though you should correct your typo and rename the exercise.php file to Exercise.php and the classname to Exercise.
Laravel 8.x Docs - Authorization - Writing Policies - Methods without Models
Laravel 8.x Docs - Authorization - Authorization Actions using Policies - Via Controller Helpers - Actions That Don't Require Models
I had this same issue and what I learnt was that Policies would work only on authenticated route.
Make sure your request is authenticated while implementing policies.

"ReflectionException Function () does not exist" when trying to setup authentication in Laravel

I'm having some trouble getting authentication working 100% in Laravel (all views seem to work so far except for "home") and was hoping to get some assistance from the Laravel experts out there.
A bit of background info:
PHP Version: 7.3.11
Laravel Version: 8.13.0
Used Composer to build the ui scaffolding (composer require laravel/ui)
Used the Bootstrap ui option (php artisan ui bootstrap --auth)
The issue
As mentioned above, I seem to be able to access all of the generated authentication views so far (login, register & the password reset views), however after registering with a dummy account I get the following error when trying to access the "home" view:
ReflectionException
Function () does not exist
The Stack trace is pointing to the following file:
"vendor/laravel/framework/src/Illuminate/Routing/RouteSignatureParameters.php:23":
<?php
namespace Illuminate\Routing;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
use ReflectionFunction;
use ReflectionMethod;
class RouteSignatureParameters
{
/**
* Extract the route action's signature parameters.
*
* #param array $action
* #param string|null $subClass
* #return array
*/
public static function fromAction(array $action, $subClass = null)
{
$parameters = is_string($action['uses'])
? static::fromClassMethodString($action['uses'])
: (new ReflectionFunction($action['uses']))->getParameters();
return is_null($subClass) ? $parameters : array_filter($parameters, function ($p) use ($subClass) {
return Reflector::isParameterSubclassOf($p, $subClass);
});
}
/**
* Get the parameters for the given class / method by string.
*
* #param string $uses
* #return array
*/
protected static function fromClassMethodString($uses)
{
[$class, $method] = Str::parseCallback($uses);
if (! method_exists($class, $method) && Reflector::isCallable($class, $method)) {
return [];
}
return (new ReflectionMethod($class, $method))->getParameters();
}
}
With the following line (line 23) being highlighted as the error:
: (new ReflectionFunction($action['uses']))->getParameters();
And here are the routes used in the "web.php" file:
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome', ['pageTitle' => 'Home']);
});
Route::get('/services', function () {
return view('services', ['pageTitle' => 'Services']);
});
Route::get('/contact', function () {
return view('contact', ['pageTitle' => 'Contact']);
});
Auth::routes();
Route::get('/home', ['pageTitle' => 'Client Dashboard'], [App\Http\Controllers\HomeController::class, 'index'])->name('home');
After a bit of googling I've learnt that the method I'm trying to use to setup authentication has been deprecated and it is now advised to use Jetstream or Fortify, however I also found a few examples of people still managing to use this old method in their projects:
https://www.youtube.com/watch?v=NuGBzmHlINQ
As this is my first ever Laravel project I was really trying to just stick with the basics and not over complicate things for myself which is why I chose not to use Jetstream or Fortify and tried to stick to this older approach of setting up authentication. However I've been stuck on this for a couple of hours now and have not been able to figure out what's going wrong which is why I'm now seeking some help with it.
Happy to provide extra details/project code if needed - any help I can get with this would be really appreciated.
Btw, this also happens to be my first ever post on StackOverflow so any feedback on my question or advice on how I can improve it would also be greatly appreciated.
Cheers,
adb
Adjust your last route to include some type of 'action', by removing that second argument (as it only takes 2 arguments):
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
I have changed the web.php file.
I was getting the same error when using the following route:
Route::post('/delete/{id}',[AdminController::class],'destroyProduct');
Changing the route using this code fixed the problem:
Route::post('/delete/{id}','App\Http\Controllers\AdminController#destroyProduct');
I was getting the same error when making a simple route.
Route::get("/test", [ListPagesController::class, "index"]);
This was my code in the web.php file and I was routing my blade file in views via the controller.
When I later redirected it directly to web.php this way without using a controller, the problem was resolved.
Route::get("/test", function () {return view("main");});
I don't quite understand why but the problem is solved.
You Should add it in line 2 web.php
use App\Http\Controllers\YourControllerName;
For me, I changed:
Route::get('/branch/', [Controller::class, 'get_branch_list'])->name('branch');
To:
Route::get('/branch/', 'Controller#get_branch_list')->name('branch');

Are Sanctum and Laravel's default auth the same if not used for tokens?

I'm not quite sure about what is meant in the Laravel documentation, so I'm asking to be sure.
We have the default authentication of Laravel on one side and Sanctum on the other.
It is stated that Sanctum can either do Tokens or simply implement auth. :
For this feature, Sanctum does not use tokens of any kind. Instead, Sanctum uses Laravel's built-in cookie based session authentication services. This provides the benefits of CSRF protection, session authentication, as well as protects against leakage of the authentication credentials via XSS. Sanctum will only attempt to authenticate using cookies when the incoming request originates from your own SPA frontend (Vue.js).
Therefor if Tokens are nevers used, Sanctum is basically the same as the default Authentication method, am I correct? Basically, does it implement the default authentication and add tokens if needed on top of that? If so, what is the difference between sanctum and passport since they do the same thing but Sanctum is said to be lightweight. What does that actually mean?
Thanks for reading
Therefore if Tokens are never used, Sanctum is basically the same as the default Authentication method, am I correct?
Yes, under the hood it uses laravel's default auth.
Taking a look at the sanctum guard (below code taken fro github. It was last commited on Apr 11, sanctum 2.x)
<?php
namespace Laravel\Sanctum;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Http\Request;
class Guard
{
/**
* The authentication factory implementation.
*
* #var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* The number of minutes tokens should be allowed to remain valid.
*
* #var int
*/
protected $expiration;
/**
* Create a new guard instance.
*
* #param \Illuminate\Contracts\Auth\Factory $auth
* #param int $expiration
* #return void
*/
public function __construct(AuthFactory $auth, $expiration = null)
{
$this->auth = $auth;
$this->expiration = $expiration;
}
/**
* Retrieve the authenticated user for the incoming request.
*
* #param \Illuminate\Http\Request $request
* #return mixed
*/
public function __invoke(Request $request)
{
if ($user = $this->auth->guard(config('sanctum.guard', 'web'))->user()) {
return $this->supportsTokens($user)
? $user->withAccessToken(new TransientToken)
: $user;
}
if ($token = $request->bearerToken()) {
$model = Sanctum::$personalAccessTokenModel;
$accessToken = $model::findToken($token);
if (! $accessToken ||
($this->expiration &&
$accessToken->created_at->lte(now()->subMinutes($this->expiration)))) {
return;
}
return $this->supportsTokens($accessToken->tokenable) ? $accessToken->tokenable->withAccessToken(
tap($accessToken->forceFill(['last_used_at' => now()]))->save()
) : null;
}
}
/**
* Determine if the tokenable model supports API tokens.
*
* #param mixed $tokenable
* #return bool
*/
protected function supportsTokens($tokenable = null)
{
return $tokenable && in_array(HasApiTokens::class, class_uses_recursive(
get_class($tokenable)
));
}
}
If you check the _invoke() method,
if ($user = $this->auth->guard(config('sanctum.guard', 'web'))->user()) {
return $this->supportsTokens($user)
? $user->withAccessToken(new TransientToken)
: $user;
}
the authenticated user is found using
$user = $this->auth->guard(config('sanctum.guard', 'web'))->user()
After checking the sanctum config file, there is no sanctum.guard config currently (it's probably meant for some future version), so sanctum checks with the web guard by default, so it's basically doing the same thing as your default web routes.
But you've misunderstood the use of Sanctum. Sanctum is for API authentication and not for web auth (though it can be used web auth as well). Sanctum's non-token auth is for your SPA's to be able to use the same API as mobile applications ( which use token authentication ) without needing tokens and providing the benefits of csrf and session based auth.
To help you understand better, suppose you have build an API which uses tokens (if it's already using sanctum for tokens, that makes things simpler) for authentication. Now you wish to build an SPA ( which may be build inside the laravel project itself, or a seperate project, on same domain or on different domain ) which will use the same API's, but since this will be built by you, it is a trusted site so you don't want it to use tokens but instead use laravel's default session based auth along with csrf protection while also using the same api routes. The SPA will communicate with the server through ajax. You also want to ensure that only your SPA is allowed to use session based auth and not allow other third party sites to use it.
So this is where Sanctum comes in. You would just need to add the Sanctum middleware to your api route group in app/Http/Kernel.php
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
'api' => [
EnsureFrontendRequestsAreStateful::class,
'throttle:60,1',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
Then configure sanctum to allow your SPA's domain and configure cors (check the docs to learn how to do this). Then just add the auth:sanctum middleware to your route and you're done with the serverside setup.
Now these routes will authenticate users if the request has a token or if it is stateful (session cookie).
Now your SPA can communicate with your API without tokens.
To get csrf protection, call the csrf-cookie request first, this will set up a csrf token in your cookies, and axios will automatically attach it to subsequent requests
axios.get('/sanctum/csrf-cookie').then(response => {
// Login...
})
What is the difference between sanctum and passport since they do the same thing but Sanctum is said to be lightweight.
Well it's just like it says, sanctum is lightweight. This is because Passport provides full Oauth functionality while Sanctum only focuses on creating and managing tokens. To explain Oauth in a simple way, you must have seen those Sign in with Google, Sign in with Facebook, Sign in with Github on different sites, and you can then sign it to those sites using your google/facebook/github account. This is possible because Google, Facebook and Github provide Oauth functionality (just a simple example, not going in to too much detail). For most websites, you don't really need Passport as it provides a lot features that you don't need. For simple api authentication Sanctum is more than enough
NOTE: This answer is for Laravel Sanctum + same-domain SPA
To add to these answers, the default Laravel auth uses the web guard, so you must use that for your auth routes (for same-domain SPA app).
For example, you can create your own routes that point to Laravel's RegistersUsers trait and AuthenticatesUsers trait:
web.php
Route::group(['middleware' => ['guest', 'throttle:10,5']], function () {
Route::post('register', 'Auth\RegisterController#register')->name('register');
Route::post('login', 'Auth\LoginController#login')->name('login');
Route::post('password/email', 'Auth\ForgotPasswordController#sendResetLinkEmail');
Route::post('password/reset', 'Auth\ResetPasswordController#reset');
Route::post('email/verify/{user}', 'Auth\VerificationController#verify')->name('verification.verify');
Route::post('email/resend', 'Auth\VerificationController#resend');
Route::post('oauth/{driver}', 'Auth\OAuthController#redirectToProvider')->name('oauth.redirect');
Route::get('oauth/{driver}/callback', 'Auth\OAuthController#handleProviderCallback')->name('oauth.callback');
});
Route::post('logout', 'Auth\LoginController#logout')->name('logout');
But make sure they are in the web.php file. For example if they are in the api.php file, I saw some weird errors about session store not on request and RequestGuard::logout() is not a function. I believe this has something to do with the default auth guard via $this->guard() in the auth traits, and something to do with api.php's /api prefix.
The /api prefix seemed related because if you use the composer package Ziggy to achieve route('login') and route('logout'), they actually resolve to /api/login and /api/logout.
I suspect that caused an issue with Sanctum. The fix was to make sure the routes were in web.php. A person may reproduce this error if their config is similar, or for example, if they have Auth::routes() declared in api.php.
Double check your Kernel.php (it should be like this):
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'throttle:60,1',
],
];
If you have StartSession in your api middleware group, your config is incorrect or unnecessarily-convoluted.
Here is my working ./config/auth.php file for your comparison:
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
...
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
Then, you can use the guest middleware for login/registration routes, and very importantly, you should then declare all your JSON-serving endpoints in api.php and use the auth:sanctum middleware on those routes.
Once you think you have it working, I have two test/debug steps for you:
One:
open Chrome > dev tools pane
goto the Applications tab
check to ensure there are two cookies: <app_name>_session, and XSRF-TOKEN
with the remember me checkbox and remember: true in login payload, ensure there is a third cookie for remember_web_<hash>
make sure the session cookie is httpOnly, and make sure the CSRF cookie is not (so your JavaScript can access it)
Two, in your unit tests, ensure that after $this->postJson(route('login'), $credentials), you see this:
Auth::check() should return true
Auth::user() should return the user object
Auth::logout() should log the user out, and immediately following that, $this->assertGuest('web'); should return true
Don't get too excited until you verify those two steps, and do get excited once you successfully verify those steps. That will mean you are using Laravel's default auth logic.
For good measure, here is an example of attaching the CSRF token via JavaScript:
import Cookies from 'js-cookie';
axios.interceptors.request.use((request) => {
try {
const csrf = Cookies.get('XSRF-TOKEN');
request.withCredentials = true;
if (csrf) {
request.headers.common['XSRF-TOKEN'] = csrf;
}
return request;
} catch (err) {
throw new Error(`axios# Problem with request during pre-flight phase: ${err}.`);
}
});
Sanctum has two separate authentication systems, there is the cookie based session authentication meant to use for single page applications, where you are sending api requests (ajax, fetch etc.) instead of server side rendering (which reads the session cookie on every page load), sanctum makes it possible to use this cookie (the default authentication) without page reloads.
The second authentication system is token based and is meant to use for mobile applications.

Laravel 7 force user to enter password

I want to know how can I force user to re-enter his password on certain route action?
Let say if user wants to edit product before save process happens he has to input his password and if it was correct then saving is allowed.
Is there any default middleware for password in laravel 7? or should i make myself?
What do you suggest?
Update
I've found this but it's only work on web routes, i need same thing to work on API routes.
Problem with this middleware is that it except sessions which is only available on web middlewares. I tried to add \Illuminate\Session\Middleware\StartSession::class, into api middleware but still getting this error
RuntimeException: Session store not set on request.
Why not just rewrite shouldConfirmPassword() method?
Session is only used to check when was the password confirmed at. If you are requiring password confirmation every time, just rewrite the method to return true:
/**
* Determine if the confirmation timeout has expired.
*
* #param \Illuminate\Http\Request $request
* #return bool
*/
protected function shouldConfirmPassword($request)
{
return true;
//$confirmedAt = time() - $request->session()->get('auth.password_confirmed_at', 0);
//return $confirmedAt > config('auth.password_timeout', 10800);
}
For those who stumble on this, the solution is setting config('password_timeout') to 1 i.e 1 second.
'password_timeout' => 1,
You might think that setting it to 0 should work but it doesn't and that's because of this line in the constructor of the Illuminate\Auth\Middleware\RequirePassword class
$this->passwordTimeout = $passwordTimeout ?: 10800;
which defaults $this->passwordTimeout to 3 hours when config('password_timeout') is not set (null) or 0.
The RequirePassword middleware is being bound to the application container in the registerRequirePassword method of the Illuminate\Auth\AuthServiceProvider class

Resources