filamentphp / filament

A collection of beautiful full-stack components for Laravel. The perfect starting point for your next app. Using Livewire, Alpine.js and Tailwind CSS.

Home Page:https://filamentphp.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bug: Filament login AuthenticationException when user password is changed directly from migrate:fresh --seed or tinker

yob-yob opened this issue · comments

commented

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:

  1. 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
]);
  1. Run php artisan migrate:fresh --seed
  2. Login to Filament Admin (you should be redirected to the dashboard after this)
  3. Run php artisan migrate:fresh --seed AGAIN
  4. Refresh the browser
  5. ERROR: Route [login] not defined.

Another way to reproduce

  1. 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
]);
  1. Run php artisan migrate:fresh --seed
  2. Login to Filament Admin (you should be redirected to the dashboard after this)
  3. change the password of the user via tinker
    \Filament\Models\User::find(1)->update([ 'password' => Hash::make('12334567890') ]);
  4. Refresh the browser
  5. 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

  1. Create a redirect route for login that would send you back to the filament.auth.login route
  2. 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 from Filament redirect them back to filament.auth.login but if they are coming from your App's protected routes you should redirect them back to you own login route some thing app.auth.login

Example for a redirect

Route::redirect('/login', '/admin/login', 301)->name('login');
commented

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...

commented
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 to Illuminate\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 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.

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 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.

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:

  1. link admin (no multi-tenancy panel) in navigation menu of app 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),
    ])
  1. link app (multi-tenancy panel and also the default panel) in OrganizationResource action of admin 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's panel() configuration.

    • I did not understand once I have the app panel as default.
  • 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 on admin 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).
  • 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 is null 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 the app panel does not have any resource, it has only the dashboard page and the admin link as navigation items.

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?