Log In as a User: Building Impersonation Without Building a Backdoor

Support keeps needing to log into a user's account, and 'ask for the password' is not a policy. How to build impersonation into a web app so the support workflow works, the session is unmistakable, the audit trail is honest, and the feature doesn't become the softest backdoor in the codebase.

The support request that keeps coming back

A user swears their dashboard shows last month's numbers. You can't reproduce it with your account, and asking them for their password is a policy violation in both directions: your side shouldn't ask, their side shouldn't give it. The realistic options narrow fast, and "log in as the user for ten minutes, see what they see, then leave" is what support actually needs.

Impersonation is a legitimate feature. It's also the softest target in a codebase if it's bolted on, because done naively every admin session is one URL away from being any user. The design decides which of those you ship.

A separate kind of session, not a token swap

The mistake to avoid is pretending impersonation is a login. Swapping the authenticated user destroys the admin's session, so ending the impersonation becomes a fresh login, and there is no record of who was actually driving the account.

Instead, keep the admin's real identity in the session and add a second one on top:

  • The session stores an impersonator_id alongside the active user id. That single fact makes every other guard in this article possible.
  • The app renders a visible banner on every page while impersonating: whose account, since when, and a one-click exit that drops back into the admin's own session. If a support engineer forgets to end an impersonation, the user's next visit makes it obvious.
  • While impersonating, actions that change state carry the account owner as the actor, but the audit log also records the impersonator. "Who changed this" should never answer "the customer" when it was you.

A dedicated package works, but the core is small enough to hand-roll in an evening, and then you understand every piece of it.

Guardrails that make it defensible

Each of these is cheap to add and earns its keep the first time something goes sideways:

  • Gate the capability, not the route alone. A permission distinct from "admin", assigned deliberately. The person who manages billing doesn't automatically get to browse anyone's account.
  • Never impersonate up. An admin cannot impersonate another admin or owner. If two admins need to debug each other's view of the system, admin-level test accounts solve that without a hole in the wall.
  • Exclude the secrets. API keys, tokens, password fields, and payment details should render masked for everyone, regardless of session. If impersonation can read a live API key, your audit trail doesn't matter; the key is on the screen.
  • Read-first, act-deliberately. Most impersonation sessions are diagnosis: look at the page, find the bug, get out. When a state change is needed, prefer doing it as a documented support action that references the ticket, and let impersonation cover the rest. The narrower the write surface, the smaller the blast radius of any mistake.
  • Timebox. Flag an impersonation session older than a working day, since a session nobody ended tends to survive a year unnoticed.

That last combination, exclusions plus timebox, is what separates a support tool from a backdoor. A backdoor is a path that works when nobody legitimate is watching.

Audit it like you don't trust anyone, because one day you shouldn't

Log the start and end of every impersonation session: who, whom, when, from where, and if you can, the ticket or conversation that justified it. Log state-changing actions performed during impersonation with both the acting user and the impersonator. Then test the negative cases as ruthlessly as the positive ones:

it('blocks impersonation of higher-privileged users', function () {
    actingAs($admin)->post('/impersonate', [
        'user_id' => $owner->id,
    ])->assertForbidden();
});

A test suite that only proves impersonation works doesn't cover the dangerous half. The forbidden cases are the product.

What to do without it

Plenty of apps never ship impersonation, and that's fine: a screen-sharing call reproduces most bugs, structured request logs with the user's context answer "what did they actually see", and a per-user debug page (visible to the user, not the admin) settles the recurring "my screen differs from yours" argument outright. Impersonation is the tool of last resort, and it should be built like one: small, loud, audited, and harder to reach than every other feature in the app.