Laravel Magazine
Building a "Login As" User Impersonation Feature for Support Admins

Building a "Login As" User Impersonation Feature for Support Admins

Eric Van Johnson ·

"I click the button and nothing happens" is a lot easier to debug when you can actually see the account it's happening on. This tutorial builds a "login as" feature that lets support staff at PHP Architect temporarily view the app as a specific customer, safely and with a full audit trail.

Step 1: Track who's impersonating whom

Schema::create('impersonation_logs', function (Blueprint $table) {
    $table->id();
    $table->foreignId('admin_id')->constrained('users');
    $table->foreignId('target_id')->constrained('users');
    $table->timestamp('started_at');
    $table->timestamp('ended_at')->nullable();
    $table->timestamps();
});

Never skip the audit log on a feature like this. If a customer asks "who was in my account on Tuesday," you need a real answer.

Step 2: The impersonation service

namespace App\Services;

use App\Models\ImpersonationLog;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;

class ImpersonationService
{
    public function start(User $admin, User $target): void
    {
        abort_if($target->is_admin, 403, 'Cannot impersonate an admin account.');

        Session::put('impersonator_id', $admin->id);

        $log = ImpersonationLog::create([
            'admin_id' => $admin->id,
            'target_id' => $target->id,
            'started_at' => now(),
        ]);

        Session::put('impersonation_log_id', $log->id);

        Auth::login($target);
    }

    public function stop(): void
    {
        $adminId = Session::pull('impersonator_id');
        $logId = Session::pull('impersonation_log_id');

        if (! $adminId) {
            return;
        }

        ImpersonationLog::whereKey($logId)->update(['ended_at' => now()]);

        Auth::login(User::findOrFail($adminId));
    }

    public function isImpersonating(): bool
    {
        return Session::has('impersonator_id');
    }
}

Storing the admin's ID in the session — rather than swapping auth guards — keeps this simple: Auth::login() on the target account does the heavy lifting, and stop() logs straight back in as the original admin.

Step 3: Routes and controller

Route::middleware(['auth', 'can:impersonate,App\Models\User'])->group(function () {
    Route::post('/admin/users/{user}/impersonate', [ImpersonationController::class, 'start']);
    Route::post('/admin/stop-impersonating', [ImpersonationController::class, 'stop']);
});
namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\ImpersonationService;

class ImpersonationController extends Controller
{
    public function __construct(private ImpersonationService $impersonation) {}

    public function start(User $user)
    {
        $this->impersonation->start(auth()->user(), $user);

        return redirect()->route('dashboard')
            ->with('status', "Now viewing as {$user->name}.");
    }

    public function stop()
    {
        $this->impersonation->stop();

        return redirect()->route('admin.users.index')
            ->with('status', 'Impersonation ended.');
    }
}

The can:impersonate gate is doing real work here — never let this route be reachable by anything less than a tightly scoped permission.

Step 4: A visible banner, always

The single most important UX rule for impersonation: the person impersonating must never forget they're doing it, and it must be obvious how to stop.

@if(app(App\Services\ImpersonationService::class)->isImpersonating())
    <div class="bg-amber-500 text-black text-center py-2 font-semibold">
        You are viewing as {{ auth()->user()->name }}.
        <form action="{{ route('admin.stop-impersonating') }}" method="POST" class="inline">
            @csrf
            <button type="submit" class="underline">Stop impersonating</button>
        </form>
    </div>
@endif

Guardrails worth keeping

A few things this feature should never do: allow impersonating another admin, allow impersonation to touch billing or account-deletion actions (gate those separately, checking Session::has('impersonator_id') and blocking if true), or let a session silently expire back into the admin's own account without ending the log entry. A scheduled command that closes any impersonation_logs row still open after, say, four hours is cheap insurance against a support agent forgetting to click "stop."

Done well, impersonation turns a fifteen-message support thread into a two-minute screen-share-with-yourself. Done carelessly, it's a serious trust and security problem — so keep the audit trail, the banner, and the admin-blocking gate non-negotiable.

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.