Bug: Filament login AuthenticationException when user password is changed directly from migrate:fresh --seed or tinker
yob-yob opened this issue · comments
Describe the bug
TL;DR
When you login on filament admin then refresh your database with a seeder it throws an AuthenticationException
error.. this error is thrown by AuthenticateSession.php
I traced the problem and it was because Authenticate.php
middleware still return a filament user... this is because SessionGuard only gets user data from the session which is still alive since we only freshened out the database and not the session then this passes to the $next
middleware AuthenticateSession.php
which checks if password_hash_filament
matches the password hash in the database (of course this check would fail since the password hash is now changed due to the refresh)... a logout method is now executed which throws an AuthenticationException
Error... this error should automatically redirect if a value of RedirectTo
is provided... unfortunately AuthenticateSession.php
does not provide this value, hence Laravel chooses a default route to login
...
To reproduce
Steps to reproduce the behavior:
- Create a Seeder for a filament user
\Filament\Models\User::create([
'name' => 'Grover Sean Reyes',
'email' => 'gsean@copypaste.ph',
'password' => Hash::make('123qwe123'),
'is_admin' => true
]);
- Run
php artisan migrate:fresh --seed
- Login to Filament Admin (you should be redirected to the dashboard after this)
- Run
php artisan migrate:fresh --seed
AGAIN - Refresh the browser
- ERROR: Route [login] not defined.
Another way to reproduce
- Create a Seeder for a filament user
\Filament\Models\User::create([
'name' => 'Grover Sean Reyes',
'email' => 'gsean@copypaste.ph',
'password' => Hash::make('123qwe123'),
'is_admin' => true
]);
- Run
php artisan migrate:fresh --seed
- Login to Filament Admin (you should be redirected to the dashboard after this)
- change the password of the user via
tinker
\Filament\Models\User::find(1)->update([ 'password' => Hash::make('12334567890') ]);
- Refresh the browser
- ERROR: Route [login] not defined.
Expected behavior
A redirect to the filament login page
Screenshots
If applicable, add screenshots to help explain your problem.
Context
- Package version: 1.10.9
- Laravel version: 8.55.0
- Server OS: MacOS
- PHP version: 7.4.22
Additional details
Is there any way to catch the mismatch of password hash before AuthenticateSession.php
is executed?
Is this even a Filament Issue in the first place? (I could see how Laravel is quite opinionated in here)
Quick Fix
- Create a redirect route for
login
that would send you back to thefilament.auth.login
route - if you're currently using
Login
route for your App login page... I still suggest you create a redirect route for login and check where the user is coming from and if the user is coming fromFilament
redirect them back tofilament.auth.login
but if they are coming from your App's protected routes you should redirect them back to you own login route some thingapp.auth.login
Example for a redirect
Route::redirect('/login', '/admin/login', 301)->name('login');
This error is not found on Laravel's Jetstream package since that package or should I say Fortify uses a login
route... but if you change the route name from the fortify package the error would still be the same... that is why I suggest a redirect route for login
I don't really know if this issue is a Filament Specific since packages like Fortify could still encounter this issue ONLY IF you change the route name that the package is using...
public function testFilamentLogin()
{
$this->withoutExceptionHandling(['Illuminate\Auth\AuthenticationException']);
$this->expectException(RouteNotFoundException::class);
// World
$user = User::factory()->create();
$this->actingAs($user, 'filament');
// Pre-Assertions
$response = $this->get('/admin');
$response->assertStatus(200);
// Action
$user->update(['password' => Hash::make('test')]);
// Assertions
$response = $this->get('/admin');
}
I've hit this before and not been able to find a solution :( I think it's a Laravel authentication limitation. If you find a solution, feel free to PR it :)
I'm having similar issue in admin panel v2 as described here laravel/framework#27105
Referring to this related issue laravel/framework#37393
I followed one of the suggestion to use Session::flush()
to clear all session data by listening to Illuminate\Auth\Events\Logout
event
@danharrin is this workaround would be possible to be implemented in admin panel v2?
I'm having similar issue in admin panel v2 as described here laravel/framework#27105
Referring to this related issue laravel/framework#37393 I followed one of the suggestion to use
Session::flush()
to clear all session data by listening toIlluminate\Auth\Events\Logout
event@danharrin is this workaround would be possible to be implemented in admin panel v2?
Just tried this solution and it worked.
I'm Using | |
---|---|
laravel/framework | 10.x |
filament/filament | 2.17.21 |
php | 8.1 |
OS | windows |
I had this same issue and got annoyed each time I refreshed the database and got the Exception.
Turns out, it is actually quite the easy fix. I don't know how this would be implemented on a filament/filament
level, but this definitely works for me.
You gotta edit your applications app\Exceptions\Handler
class. The extended ExceptionHandler
class has a protected function called unauthenticated
and that is where the route('login')
is hard coded.
Simply override that function and you're golden.
Here's my class, with some lines removed for brevity:
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
protected function unauthenticated($request, AuthenticationException $exception)
{
return $this->shouldReturnJson($request, $exception)
? response()->json(['message' => $exception->getMessage()], 401)
: redirect()->guest($exception->redirectTo() ?? route('filament.auth.login'));
}
}
As you can see here, I opted to use Filament's filament.auth.login
named route.
Now if you use Filament as an admin panel only, and have a different user portal, you're most likely using custom guards. The $exception
variable in the function contains the used guard in a guards()
method. Using this you could easily change where it redirects to.
I'm Using
laravel/framework 10.x
filament/filament 2.17.21
php 8.1
OS windows
I had this same issue and got annoyed each time I refreshed the database and got the Exception.Turns out, it is actually quite the easy fix. I don't know how this would be implemented on a
filament/filament
level, but this definitely works for me.You gotta edit your applications
app\Exceptions\Handler
class. The extendedExceptionHandler
class has a protected function calledunauthenticated
and that is where theroute('login')
is hard coded.Simply override that function and you're golden.
Here's my class, with some lines removed for brevity:
<?php namespace App\Exceptions; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler { public function register(): void { $this->reportable(function (Throwable $e) { // }); } protected function unauthenticated($request, AuthenticationException $exception) { return $this->shouldReturnJson($request, $exception) ? response()->json(['message' => $exception->getMessage()], 401) : redirect()->guest($exception->redirectTo() ?? route('filament.auth.login')); } }
As you can see here, I opted to use Filament's
filament.auth.login
named route.Now if you use Filament as an admin panel only, and have a different user portal, you're most likely using custom guards. The
$exception
variable in the function contains the used guard in aguards()
method. Using this you could easily change where it redirects to.
this solved my issue than you.
I'm Using
laravel/framework 10.x
filament/filament 2.17.21
php 8.1
OS windows
I had this same issue and got annoyed each time I refreshed the database and got the Exception.Turns out, it is actually quite the easy fix. I don't know how this would be implemented on a
filament/filament
level, but this definitely works for me.You gotta edit your applications
app\Exceptions\Handler
class. The extendedExceptionHandler
class has a protected function calledunauthenticated
and that is where theroute('login')
is hard coded.Simply override that function and you're golden.
Here's my class, with some lines removed for brevity:
<?php namespace App\Exceptions; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler { public function register(): void { $this->reportable(function (Throwable $e) { // }); } protected function unauthenticated($request, AuthenticationException $exception) { return $this->shouldReturnJson($request, $exception) ? response()->json(['message' => $exception->getMessage()], 401) : redirect()->guest($exception->redirectTo() ?? route('filament.auth.login')); } }
As you can see here, I opted to use Filament's
filament.auth.login
named route.Now if you use Filament as an admin panel only, and have a different user portal, you're most likely using custom guards. The
$exception
variable in the function contains the used guard in aguards()
method. Using this you could easily change where it redirects to.
it works, thank you!
anyone from google:
i always used the pre-hashed string for my user factory passwords
a recent laravel version has changed this logic so now the password hash changes every single time you migrate and seed
therefore, i was suddenly seeing this page in filament every time i re-seeded during dev
the solution was to just change my user factory password back to the hard-coded string from previous laravel versions:
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
I've hit this before and not been able to find a solution :( I think it's a Laravel authentication limitation. If you find a solution, feel free to PR it :)
I fixed this issue by updating the redirectTo function of AuthenticateSession Middleware.
protected function redirectTo(Request $request)
{
// Check if the request URL contains the admin panel path
if (str_contains($request->url(), '/admin')) {
return '/admin/login'; // Redirect to admin login if in the admin panel
}
// Default redirect to the regular login
return '/login';
}
In my case the problem occurs when the user change his own password. I realized that the problem is because I have two panels and also changed the start path of both: one to /app
and another to /admin
.
So when the AuthenticateSession
middleware identifies that the user is not logged in anymore, it redirects to the /login
route that does not exists raising an exception.
I ended using the same approach as @hammadzafar05 but I made it more versatile to identify the panel and do a named route redirect return the login url.
EDIT: The code was changed after @danharrin question below.
protected function redirectTo(Request $request)
{
return Filament::getCurrentPanel()?->getLoginUrl();
}
Doesnt $panel->getLoginUrl()
work?
Doesnt
$panel->getLoginUrl()
work?
Yes. It worked. I updated my answer above.
I'm aware of the usage of Filament::getLoginUrl()
directly that handles the current panel for me, but it does not have the null safe operator. In reality none of the direct methods on FilamentManager
have the null safe operator. Maybe we need a PR to fix this?
Also, FYI, I did not used it before because I was getting error in another situation. I have two panels as stated above and want to:
- link
admin
(no multi-tenancy panel) in navigation menu ofapp
panel
$panel
->navigationItems([
NavigationItem::make('Admin')
->url(fn () => url(Filament::getPanel('admin')->getPath()))
->icon('heroicon-o-wrench-screwdriver')
->visible(function () {
/** @var \App\Models\User */
$user = auth()->user();
return $user->isAdmin();
})
->sort(1),
])
- link
app
(multi-tenancy panel and also the default panel) inOrganizationResource
action ofadmin
panel
Action::make('access_app')
->label('filament.actions.access_app')
->translateLabel()
->icon('heroicon-m-arrow-top-right-on-square')
->visible(function (Organization $record): bool {
/** @var \App\Models\User */
$user = auth()->user();
return $user->canAccessTenant($record);
})
->url(function (Organization $record): string {
$app = Filament::getPanel('app')->getPath();
$path = "$app/$record->slug";
return url($path);
})
Notice that I used getPath()
on both cases.
This is because the getUrl()
(and also getLoginUrl()
) is giving me:
- On 1, if used without a callback
->url(Filament::getPanel('admin')->getUrl())
-
Error: No default Filament panel is set. You may do this with the
default()
method inside a Filament provider'spanel()
configuration.- I did not understand once I have the
app
panel as default.
- I did not understand once I have the
-
On 1, if used within a callback
->url(fn () => Filament::getPanel('admin')->getUrl())
-
Error: Route [filament.app.resources.organizations.index] not defined.
- The panel
app
does not really have an organizations resource. This resource is the first resource onadmin
panel. I guess that it got wrong concatenating the default panel (app
) with the first navigation item from the panel that I actually requested the URL (admin
).
- The panel
-
On 2, if used as
->url(fn (Organization $record): string => Filament::getPanel('app')->getUrl($record))
- Error: App\Filament\Admin\Resources\OrganizationResource::App\Filament\Admin\Resources{closure}(): Return value must be of type string, null returned
- This error I traced and got that
$firstGroup
isnull
because$navigation
is an empty array. I guess maybe something while trying to set the tenant, because the existence of two panels or because that theapp
panel does not have any resource, it has only the dashboard page and the admin link as navigation items.
- This error I traced and got that
But probably I misunderstood the concept or usage of something at all. I'm a newbie with Filament: just four days reading docs and two coding.
Do you think I may create a separate issue/discussion and move this into there?