Security
Account switching touches authentication, so here is exactly what the plugin guarantees and what stays your responsibility.
Every switch goes through one path
Impersonation, linked-account switches and developer logins all end in the same internal method. It:
- Re-authenticates the panel's own guard as the target (
Auth::guard($panel->getAuthGuard())->login()), never a guard of its own. - Regenerates the session id, so a session fixed before the switch is useless after it.
- Clears Laravel's per-guard
password_hash_*session keys soAuthenticateSessiondoes not immediately log the new user out — and does log them out again if their password changes later, as usual. - Writes the audit row and dispatches the events.
Nothing is stored in cookies beyond the regular session cookie; "remember me" is never set by a switch.
Linked accounts
Both accounts must be proven. Linking an existing account requires that account's e-mail and password. Creating a sub-account happens from the account that will own it. A user can only ever link accounts they can already sign in to.
Escalation asks for the target's password. Switching to an account whose link has requires password on means entering the target account's password. A compromised low-privilege session therefore cannot reach the admin account without the admin password. The reverse direction of any new link requires a password by default; only someone signed in as that account can relax it.
Panel access is re-checked. You cannot switch to a linked account that cannot access any of your panels (canAccessPanel()), and after switching you land on a panel it may access.
Your rules run too. canSwitchUsing() lets you require 2FA on the target, block locked accounts, or keep switches inside a tenant.
No escalation through impersonation
While impersonating, the "Switch to" menu is hidden, the Linked accounts page returns 403, and switchToLinkedAccount() throws. Otherwise an admin impersonating a user could hop into that user's linked admin account — or a user could set up a link precisely to be "impersonated into". Nested impersonation is denied for the same reason.
Impersonation
- The action is hidden and the service throws unless
canImpersonate()on the impersonator,canBeImpersonated()on the target, andcanImpersonateUsing()on the plugin all agree (each only if defined). - Define
canImpersonate()on your model or setcanImpersonateUsing(). With neither, any panel user can impersonate any other — fine for a prototype, not for production. - The impersonator's id is kept in the session, not the URL; Switch back is a CSRF-protected
POSTinside the panel's authenticated route group. - A banner is always visible while impersonating so nobody forgets which account they are acting as.
Developer logins
- Two gates, both server-side: the environment allow-list in config (
['local']by default) and the plugin's user list. A button that somehow rendered elsewhere would still get a 404 on submit. - The route is throttled (10/minute) and CSRF-protected.
- Keep the environment list tight. Never add
production.
Audit trail
Each switch records who, to whom, why, which panel and guard, IP and user agent. Point a Filament resource at AccountSwitch to review it, or listen to AccountSwitched to alert in real time. If you disable log_switches, keep your own listener — silent switching is the one thing this plugin should never do.
Reporting a vulnerability
E-mail support@packstub.dev rather than opening a public issue. You will get a reply within two business days.