Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Laravel Nova does not respect nova-impersonate guard config #6796

Closed
steffanhalv opened this issue Mar 25, 2025 · 9 comments
Closed

Laravel Nova does not respect nova-impersonate guard config #6796

steffanhalv opened this issue Mar 25, 2025 · 9 comments
Labels
needs more info More information is required

Comments

@steffanhalv
Copy link

steffanhalv commented Mar 25, 2025

  • Laravel Version: 11.43.0
  • Nova Version: 4.27.0
  • PHP Version: 8.3.17
  • Database Driver & Version: Mysql 8.2
  • Operating System and Version: Unrelated
  • Browser type and version: Unrelated
  • Reproduction Repository: Not needed

Description:

Laravel Nova does not respect nova-impersonate guard config when multiple guards using same provider and driver.

Detailed steps to reproduce the issue on a fresh Nova installation:

config/nova-impersonate.php

<?php

return [

    'default_impersonator_guard' => 'nova',
    'impersonator_guards' => ['nova'],

Notice: In laravel < 10 this would work as Laravel Nova selects the first defined guard, but in laravel 11 the web guard is always added to the top

Related Laravel issue: laravel/framework#55165
config/auth.php

'guards' => [
        'nova' => [
            'driver' => 'session',
            'provider' => 'users',
        ],

        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ]

The authGuard will always resolve to 'web' from Laravel 11 and up ...

vendor/laravel/nova/src/Http/Controllers/ImpersonateController.php

public function startImpersonating(NovaRequest $request, ImpersonatesUsers $impersonator)
    {
        if ($impersonator->impersonating($request)) {
            return $this->stopImpersonating($request, $impersonator);
        }

        /** @var class-string<\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model> $userModel */
        $userModel = with(Nova::modelInstanceForKey($request->input('resource')), function ($model) {
            return ! is_null($model) ? get_class($model) : Util::userModel();
        });

        $authGuard = Util::sessionAuthGuardForModel($userModel);

        $currentUser = Nova::user($request);

        /** @var \Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model $user */
        $user = $userModel::findOrFail($request->input('resourceId'));

        // Now that we're guaranteed to be a 'real' user, we'll make sure we're
        // actually trying to impersonate someone besides ourselves, as that
        // would be unnecessary.
        if (! $currentUser->is($user)) {
            abort_unless(optional($currentUser)->canImpersonate() ?? false, 403);
            abort_unless(optional($user)->canBeImpersonated() ?? false, 403);

            $impersonator->impersonate(
                $request,
                Auth::guard($authGuard),
                $user
            );
        }

        return $impersonator->redirectAfterStartingImpersonation($request);
    }

Because this looks for the first guard having the given driver and provider (which always will be 'web' in >= Laravel 11)

vendor/laravel/nova/src/Util.php

public static function sessionAuthGuardForModel($model)
    {
        if (is_object($model)) {
            $model = get_class($model);
        }

        $provider = collect(config('auth.providers'))->reject(function ($provider) use ($model) {
            return ! ($provider['driver'] === 'eloquent' && is_a($model, $provider['model'], true));
        })->keys()->first();

        return collect(config('auth.guards'))->reject(function ($guard) use ($provider) {
            return ! ($guard['driver'] === 'session' && $guard['provider'] === $provider);
        })->keys()->first();
    }

Solution

Always respect the default_impersonator_guard config.

@crynobone
Copy link
Member

Please provide full reproducing repository based on fresh installation as suggested in the bug report template (or you can refer to https://github.com/nova-issues for example)

@crynobone crynobone added the needs more info More information is required label Mar 25, 2025
@steffanhalv
Copy link
Author

steffanhalv commented Mar 25, 2025

Please see https://github.com/steffanhalv/laravel-config-bug for how laravel always prioritize the 'web' guard. As for the Laravel Nova bug the problem should be very simple to see based on this:

vendor/laravel/nova/src/Auth/Adapters/SessionImpersonator.php

// The $guard inserted here will always be the 'web' guard from Laravel 11 and up because it is always added to the top of the config
public function impersonate(Request $request, StatefulGuard $guard, Authenticatable $user)
    {
        return rescue(function () use ($request, $guard, $user) {
            $impersonator = Nova::user($request);

            $request->session()->put(
                'nova_impersonated_by', $impersonator->getAuthIdentifier()
            );
            $request->session()->put(
                'nova_impersonated_remember', $guard->viaRemember()
            );

            $novaGuard = config('nova.guard') ?? config('auth.defaults.guard');

            $authGuard = method_exists($guard, 'getName')
                            ? Str::between($guard->getName(), 'login_', '_'.sha1(get_class($guard)))
                            : null;

            if (is_null($authGuard)) {
                return false;
            }

            \Log::info('$novaGuard ' . $novaGuard . ' $authGuard ' . $authGuard);

            if ($novaGuard !== $authGuard) {
                $request->session()->put(
                    'nova_impersonated_guard', $authGuard
                );
            }

            $guard->login($user);

            event(new StartedImpersonating($impersonator, $user));

            return true;
        }, false);
    }

Also see how vendor/laravel/nova/src/Http/Controllers/ImpersonateController.phpSelects which guard to pass as parameter and vendor/laravel/nova/src/Util.phpfor how this guard is selected.

@steffanhalv
Copy link
Author

steffanhalv commented Mar 25, 2025

Work around

Create a new controller

app/Http/Controllers/Nova/ImpersonateController.php

<?php

namespace App\Http\Controllers\Nova;

use Illuminate\Support\Facades\Auth;
use Laravel\Nova\Http\Controllers\ImpersonateController as NovaImpersonateController;
use Laravel\Nova\Contracts\ImpersonatesUsers;
use Laravel\Nova\Http\Requests\NovaRequest;
use Laravel\Nova\Nova;
use Laravel\Nova\Util;

/**
 * This Controller overrides NovaImpersonateController to provide the correct auth guard.
 * @see: https://github.com/laravel/nova-issues/issues/6796
 */

class ImpersonateController extends NovaImpersonateController
{
    /**
     * Start impersonating a user.
     *
     * @param  \Laravel\Nova\Http\Requests\NovaRequest  $request
     * @param  \Laravel\Nova\Contracts\ImpersonatesUsers  $impersonator
     * @return \Illuminate\Http\JsonResponse
     */
    public function startImpersonating(NovaRequest $request, ImpersonatesUsers $impersonator)
    {
        if ($impersonator->impersonating($request)) {
            return $this->stopImpersonating($request, $impersonator);
        }

        /** @var class-string<\Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model> $userModel */
        $userModel = with(Nova::modelInstanceForKey($request->input('resource')), function ($model) {
            return ! is_null($model) ? get_class($model) : Util::userModel();
        });

        // @notice - Obtaining auth guard by nova config as Util resolves to wrong auth guard
        $authGuard = config('nova-impersonate.default_impersonator_guard'); // Util::sessionAuthGuardForModel($userModel);

        $currentUser = Nova::user($request);

        /** @var \Illuminate\Contracts\Auth\Authenticatable&\Illuminate\Database\Eloquent\Model $user */
        $user = $userModel::findOrFail($request->input('resourceId'));

        // Now that we're guaranteed to be a 'real' user, we'll make sure we're
        // actually trying to impersonate someone besides ourselves, as that
        // would be unnecessary.
        if (! $currentUser->is($user)) {
            abort_unless(optional($currentUser)->canImpersonate() ?? false, 403);
            abort_unless(optional($user)->canBeImpersonated() ?? false, 403);

            $impersonator->impersonate(
                $request,
                Auth::guard($authGuard),
                $user
            );
        }

        return $impersonator->redirectAfterStartingImpersonation($request);
    }
}

& overwrite the route

app/Providers/NovaServiceProvider.php

use App\Http\Controllers\Nova\ImpersonateController;

class NovaServiceProvider extends NovaApplicationServiceProvider
{

   /**
     * Register the Nova routes.
     */
    protected function routes(): void
    {
        Nova::routes()
            ->withAuthenticationRoutes(default: true)
            ->withPasswordResetRoutes()
            ->register();

        Route::middleware(['nova'])->post('/nova-api/impersonate', [ImpersonateController::class, 'startImpersonating'])->name('nova-api.start-nova-impersonation');
    }

@crynobone
Copy link
Member

Please see https://github.com/steffanhalv/laravel-config-bug for how laravel always prioritize the 'web' guard. As for the Laravel Nova bug the problem should be very simple to see based on this:

Okay, but how does that affect your application since both web and nova guard have the same provider and driver?

@steffanhalv
Copy link
Author

steffanhalv commented Mar 26, 2025

Please see https://github.com/steffanhalv/laravel-config-bug for how laravel always prioritize the 'web' guard. As for the Laravel Nova bug the problem should be very simple to see based on this:

Okay, but how does that affect your application since both web and nova guard have the same provider and driver?

They do have the same driver and provider but different logic and purpose. And because laravel nova automatically always select the web guard for impersonation while we are using the nova guard for our nova dashboard, impersonation does not work.

@crynobone
Copy link
Member

Please show the different in logic in the reproduction repository for further debug. As of now, it doesn't show any difference and shouldn't matter

@steffanhalv
Copy link
Author

steffanhalv commented Mar 26, 2025

Please show the different in logic in the reproduction repository for further debug. As of now, it doesn't show any difference and shouldn't matter

I don’t think it would be helpful to showcase our custom logic within our custom Nova guard. The issue is not with our implementation but rather with the fact that we have explicitly configured Nova Impersonate to use our nova guard in config/nova-impersonate.php by setting:

'default_impersonator_guard' => 'nova',

However, during impersonation, it still uses the web guard instead, which prevents impersonation from working as expected.

I believe the problem has been clearly explained above, especially when considering the workaround we implemented and the files I referenced. Additionally, while the Laravel issue you closed as a duplicate is related, it is not quite the same problem, I posted a new comment there also laravel/framework#55165.

At this point, I don’t think setting up a full reproduction repository is necessary, given that we have a workaround. However, the current solution is not ideal, and I still believe this is a bug that needs to be addressed.

Anyways, thanks for your time — I appreciate your help in looking into this :-)

@crynobone
Copy link
Member

'default_impersonator_guard' => 'nova',

This is not a Laravel Nova feature. Again, there is nothing here that indicates that the web and nova guards have any different logic (and to indicate there's a bug).

@steffanhalv
Copy link
Author

Uh, thx, that explains something. It must have been picked up from this repo KABBOUCHI/nova-impersonate#58 ... at some time.

Still, I can't get the impersonation to work without my work around for some reason, and in laravel 10 it worked because laravel did respect the order of which guards were defined, but now it does not because the 'web' guard is always moved to index 0.

I also tried to remove the nova session guard and only use the web auth guard for session authentication, but now it complains about not finding the nova guard.

Guess I have to investigate some more on this.

Thx for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs more info More information is required
Projects
None yet
Development

No branches or pull requests

2 participants