Linked accounts
Linked accounts let one person hold several accounts and move between them without signing out. The typical setup is a full admin account you rarely touch and a daily account with fewer permissions. The package makes the safe account the convenient one.
Linking
The trait on your user model:
$admin->linkAccount($daily, label: 'Daily', requiresPassword: false);
$admin->unlinkAccount($daily);
$admin->isLinkedTo($daily); // bool
$admin->linkedAccounts; // BelongsToMany, pivot has label + requires_password
Links are symmetric: both accounts can switch to each other. Each direction has its own label and password rule, stored on the linked_accounts pivot. Linking twice updates the label and the rule instead of duplicating the row.
When you build a page that links accounts, ask for the other account's e-mail and password before calling linkAccount(), so a user can only ever link accounts they can already sign in to:
$account = User::query()->where('email', $request->email)->first();
if (! $account || $account->is($request->user()) || ! AccountSwitcher::passwordMatches($account, $request->password)) {
throw ValidationException::withMessages(['email' => 'No account matches those credentials.']);
}
$request->user()->linkAccount($account, label: $request->label, requiresPassword: $request->boolean('requires_password'));
The "Switch to" list
use Packstub\AccountSwitcher\Facades\AccountSwitcher;
AccountSwitcher::switchableAccounts(auth()->user()); // Collection of users, empty while impersonating
AccountSwitcher::requiresPassword($from, $to); // whether that link asks for $to's password
A menu built from it:
@foreach (AccountSwitcher::switchableAccounts(auth()->user()) as $account)
<form method="POST" action="{{ route('account-switcher.switch') }}">
@csrf
<input type="hidden" name="account" value="{{ $account->getKey() }}">
@if (AccountSwitcher::requiresPassword(auth()->user(), $account))
<input type="password" name="password" placeholder="Password of {{ $account->name }}" required>
@endif
<button type="submit">{{ $account->pivot->label ?: $account->name }}</button>
</form>
@endforeach
POST account-switcher.switch takes account (the key) and password. An account that is not linked answers 403; a wrong or missing password comes back as a validation error on password, so the user stays on the form.
From code:
AccountSwitcher::canSwitchToLinkedAccount($from, $to); // bool
AccountSwitcher::switchToLinkedAccount($to, $password); // throws AccountSwitchDenied
AccountSwitcher::redirectUrlFor($to); // where to send the browser next
switchToLinkedAccount() re-authenticates the guard as the target, regenerates the session, writes an audit row, and dispatches AccountSwitching then AccountSwitched with reason SwitchReason::LinkedAccount.
The password rule
Each direction of a link has its own requires password flag:
- When you create the link (from account A to account B), you choose whether switching A to B asks for B's password.
- The reverse direction (B to A) always requires A's password until someone signed in as B relaxes it.
In practice: create your daily account from the admin account with the flag off, and you get one-click down, password-protected up. The password asked for is always the target account's, so a compromised low-privilege session cannot reach the admin account without the admin password.
Built-in checks
canSwitchToLinkedAccount() is false unless:
- The session is not impersonating (see Security).
- The target is a different account, linked to the current one.
- Linked accounts are on (
features.linked_accounts). - The target may sign in here:
canAccessUsing()on the app context, which defaults to yes. - The app context's
canSwitchUsing()callback agrees, if set.
use Packstub\AccountSwitcher\AppContext;
$this->app->make(AppContext::class)
->canAccessUsing(fn (User $user): bool => $user->is_active)
->canSwitchUsing(fn (User $from, User $to): bool => ! $to->is_locked && $to->two_factor_confirmed_at !== null);