User Impersonation ("Sign in as this user")
⚠️ Cookbook example — not live code. Every code block below is an example snippet, not part of the llamapress.ai codebase, and not running on this server. This is a reference recipe for a Leo instance (an AI coding agent) to implement in its own app — read it to understand the pattern, then recreate it there.
Impersonation lets a trusted admin become another user for a session — see exactly what
that user sees, reproduce their bug, walk them through a screen — then drop back to their
own account with one click. The whole trick is: stash the admin's real id in the session,
sign_in the target user, and show a loud banner the whole time so nobody forgets who
they are.
When to use: admin/support tooling where you need to debug a user's account from the inside. When not to: as a login shortcut for regular users, or without an audit trail if you handle sensitive data — see Gotchas.
The 80/20 in one breath
- Add two routes:
POST /impersonate/:user_id(start) andPOST /stop_impersonating(end). - On start, save
session[:impersonator_id] = current_user.id, thensign_inthe target user. - On stop,
sign_inthe user whose id is insession[:impersonator_id], then clear it. - Add two helpers —
true_user(the real admin behind the mask) andimpersonating?— toApplicationController. - Render a fixed warning banner in the layout whenever
impersonating?is true.
Devise's own sign_in swaps the session user for you. You are only bookkeeping who to
go back to.
Layer 1 — Routes
# config/routes.rb
post "/impersonate/:user_id", to: "impersonations#create", as: :impersonate
post "/stop_impersonating", to: "impersonations#destroy", as: :stop_impersonating
Both are POST on purpose — impersonating is a state change, so it must not be a GET
a crawler or a prefetch can trip.
Layer 2 — The controller
# app/controllers/impersonations_controller.rb
class ImpersonationsController < ApplicationController
before_action :authenticate_user!
before_action :ensure_admin!, only: [:create]
def create
user = User.find(params[:user_id])
# Only let an admin impersonate someone in their own organization,
# unless they're a superadmin. Scope this to YOUR authorization model.
if current_user.organization_id == user.organization_id || current_user.admin?
session[:impersonator_id] = current_user.id # remember who we really are
sign_in(:user, user) # Devise swaps the session user
redirect_to root_path, notice: "Now impersonating #{user.email}"
else
redirect_to users_path, alert: "Not authorized to impersonate this user"
end
end
def destroy
if session[:impersonator_id]
admin = User.find(session[:impersonator_id])
session.delete(:impersonator_id) # clear BEFORE sign_in, see Gotchas
sign_in(:user, admin) # become the admin again
redirect_to users_path, notice: "Stopped impersonating"
else
redirect_to root_path
end
end
private
def ensure_admin!
redirect_to root_path, alert: "Not authorized" unless current_user.admin?
end
end
destroy is deliberately not admin-gated — the current session user is the
impersonated (non-admin) person, so gating it on admin? would trap them in the target
account with no way out. The session[:impersonator_id] presence check is the only
authorization destroy needs.
Layer 3 — Helpers on ApplicationController
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :true_user, :impersonating?
# The real human behind the session. Falls back to current_user when nobody
# is impersonating, so it's always safe to call in views.
def true_user
@true_user ||= User.find_by(id: session[:impersonator_id]) if session[:impersonator_id]
@true_user ||= current_user
end
def impersonating?
session[:impersonator_id].present?
end
end
helper_method exposes both to views. Use current_user for "the account being viewed"
and true_user for "the real admin" — the distinction matters for audit logging (log
true_user.id did the action as current_user.id).
Layer 4 — The banner
Render this in your layout so it shows on every page while impersonating. A loud, fixed, high-contrast bar is the whole safety mechanism — it stops an admin from doing something in a customer's account thinking it's their own.
<%# app/views/layouts/application.html.erb (just inside <body>, above <main>) %>
<% if impersonating? %>
<div class="bg-warning text-warning-content px-4 py-2 flex justify-between items-center shadow-md">
<div>
<i class="fas fa-user-secret mr-2"></i>
You are impersonating <strong><%= current_user.email %></strong>
(Signed in as <strong><%= true_user.email %></strong>)
</div>
<%= button_to "Stop Impersonating", stop_impersonating_path, method: :post,
class: "btn btn-sm btn-outline border-warning-content hover:bg-warning-content hover:text-warning" %>
</div>
<% end %>
No Font Awesome on your box? Drop the <i> for an inline SVG or a plain "⚠" — the icon
is decoration, the text and colour carry the meaning.
Layer 5 — The trigger button
Put this on your admin user list or a user's detail page. target: "_blank" opens the
impersonated session in a new tab so the admin keeps their own tab logged in as
themselves — a small quality-of-life win that avoids the round-trip through the banner.
<%# app/views/admin/users/show.html.erb (or your user index row) %>
<%= button_to impersonate_path(@user), method: :post,
class: "btn btn-primary btn-xs", form: { target: "_blank" } do %>
<i class="fas fa-user-secret mr-1"></i> Sign in as this user
<% end %>
Gotchas (the hard-won stuff)
- Clear the session key BEFORE
sign_inindestroy, not after. Devise'ssign_inresets the session to prevent session fixation, which can wipe keys depending on your setup and ordering. Deleteimpersonator_idfirst so you never end up half-reverted (signed back in as the admin but the app still thinks you're impersonating). destroymust not be admin-gated. While impersonating,current_useris the target (usually a non-admin). Anensure_admin!ondestroylocks them in with no exit. Onlycreategets the admin gate.- Scope who can impersonate whom. The example allows same-organization or superadmin.
Never ship
User.find(params[:user_id])+sign_inwithout an authorization check — that's an account-takeover endpoint. Match the scope to your real permission model. true_uservscurrent_userfor audit logs. Any action taken while impersonating is done bycurrent_user(the target) but caused bytrue_user(the admin). Log both, or you lose all accountability. If you store sensitive data, consider recording every impersonation start/stop to a table with admin id, target id, and timestamp.- POST-only routes. A
GET /impersonate/:idwould let a link, prefetch, or crawler silently switch accounts. Keep both routesPOSTand drive them withbutton_to, notlink_to. - Banner lives in the layout, not a partial you forget to include. If it renders
per-page, the one page missing it is where the accident happens. Put it once in the
application layout, above
yield. - Impersonation ignores the target's password/2FA. That's the point, and the risk — the admin account is now a skeleton key. Protect it with strong auth (see the linked 2FA guide) because compromising one admin compromises every user they can impersonate.
Files this pattern touches
config/routes.rb
app/controllers/impersonations_controller.rb
app/controllers/application_controller.rb (true_user + impersonating? helpers)
app/views/layouts/application.html.erb (the banner)
app/views/admin/users/show.html.erb (the trigger button)
How to adapt to your schema
- Devise assumed. The pattern needs
sign_in(:user, user)andcurrent_user. On a different auth stack, replace both with your library's "set the current session user" and "read it back" calls — the session bookkeeping is identical. - Rename the resource. If your model isn't
User, swap the class and the:userscope insign_inthroughout. - Rewrite the authorization check in
createto your real rules (role column, Pundit policy, anadmin?/support?flag). This is the one line you must not copy blindly. - Drop the org scoping if you're single-tenant — keep only the
admin?gate. - Style the banner to whatever CSS you use; the only requirements are that it's impossible to miss and carries the Stop button.