DevOps and ReliabilityFull Stack Development
User Impersonation in Rails with the Pretender Gem
User impersonation lets an admin temporarily act as another user for debugging and support, and the pretender gem handles the current_user switch and session state.
· 4 min read
User impersonation lets a privileged user, typically an admin, temporarily act as another user inside the application. It sounds like it should be complicated, but the underlying mechanism is small: it changes what the application treats as current_user.
Why You’d Need This
It’s not something every app needs, but it earns its place in a few scenarios:
- Debugging and support: when a user reports a bug you can’t reproduce, impersonation lets you see the application exactly as they do.
- Testing: switching between user roles (standard, premium, new signup) without logging in and out repeatedly.
- Auditing and compliance: verifying a user’s view of certain data for compliance reasons, handled with logging and care.
What Actually Changes
If you’re using a common setup like Devise, you have a current_user helper available everywhere. Impersonation swaps what current_user returns to the impersonated user, while keeping track of who the original admin was.
The pretender Gem
You could build this from scratch, but pretender is a small, well-maintained gem that handles the underlying session mechanics.
Step 1: Add the Gem
Add pretender to your Gemfile:
# Gemfile
gem 'pretender'
Then run bundle install.
Step 2: Enable It on ApplicationController
Tell ApplicationController that it handles impersonation by adding impersonates :user (assuming your user model is User):
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
impersonates :user # Or whatever your main user model is, e.g., :admin_user
# ... your existing authentication methods, e.g., current_user, authenticate_user!
end
That line gives you impersonate_user(user_instance) and stop_impersonating_user.
Step 3: Controller Actions
These actions belong in a controller only accessible to privileged users, for example Admin::UsersController, which already lists all users.
# app/controllers/admin/users_controller.rb
class Admin::UsersController < ApplicationController
before_action :authenticate_admin! # Make sure only admins can access these actions!
def index
@users = User.order(:email)
end
def impersonate
user_to_impersonate = User.find(params[:id])
impersonate_user(user_to_impersonate) # This is where pretender shines!
redirect_to root_path, notice: "You are now impersonating #{user_to_impersonate.email}."
rescue ActiveRecord::RecordNotFound
redirect_to admin_users_path, alert: "User not found."
end
def stop_impersonating
stop_impersonating_user # And this is how you go back to being yourself.
redirect_to admin_users_path, notice: "You have stopped impersonating."
end
private
def authenticate_admin!
# Implement your admin authorization here.
# For example, if using Devise with an `admin` boolean column:
# unless current_user&.admin?
# redirect_to root_path, alert: "You are not authorized to view this page."
# end
end
end
Step 4: Routes
Add routes for these actions:
# config/routes.rb
Rails.application.routes.draw do
namespace :admin do
resources :users, only: [:index] do
post :impersonate, on: :member # For /admin/users/:id/impersonate
post :stop_impersonating, on: :collection # For /admin/users/stop_impersonating
end
end
# ... your other routes
end
Note on: :member for impersonating a specific user and on: :collection for the general “stop” action.
Step 5: Make the State Visible
The admin needs to know who they are and who they’re currently impersonating, at all times, in the UI.
In the admin users list (app/views/admin/users/index.html.erb):
<h1>Admin User Management</h1>
<% if current_user && true_user && current_user != true_user %>
<div style="background-color: #fffacd; padding: 10px; border-left: 5px solid #ffeb3b; margin-bottom: 20px;">
<strong>Heads Up!</strong> You (<%= true_user.email %>) are currently viewing the app as <%= current_user.email %>.
<%= button_to "Stop Impersonating", stop_impersonating_admin_users_path, method: :post, data: { turbo: false, confirm: "Are you sure you want to stop impersonating?" }, class: "btn btn-sm btn-warning ms-3" %>
</div>
<% end %>
<table>
<thead>
<tr>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= user.email %></td>
<td>
<% if user == current_user %>
<span class="badge bg-secondary">Current User (You)</span>
<% else %>
<%= button_to "Impersonate", impersonate_admin_user_path(user), method: :post, data: { turbo: false }, class: "btn btn-sm btn-primary" %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
Note the data: { turbo: false } on the button_to tags. Forms that change session state need a full page reload, since Turbo’s caching can otherwise leave stale state in the impersonation banner.
In the main layout (app/views/layouts/application.html.erb), add the same indicator globally:
<!DOCTYPE html>
<html>
<head>
<title>My Awesome App</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbo-track': 'reload' %>
<%= javascript_importmap_tags %>
</head>
<body>
<% if current_user && true_user && current_user != true_user %>
<div style="background-color: #f8d7da; color: #721c24; padding: 10px; text-align: center; border-bottom: 1px solid #f5c6cb;">
<i class="fas fa-exclamation-triangle"></i>
<strong>IMPERSONATION MODE:</strong> You are currently logged in as **<%= current_user.email %>**.
<%= link_to "Stop Impersonating", stop_impersonating_admin_users_path, data: { turbo_method: :post, turbo_confirm: "Are you sure you want to stop impersonating?" }, style: "color: #721c24; text-decoration: underline; margin-left: 15px;" %>
</div>
<% end %>
<%= yield %>
</body>
</html>
A banner that’s easy to miss defeats the purpose. Make it visually unmissable.
How pretender Works Under the Hood
The impersonates :user line augments current_user. Calling impersonate_user(some_user) stores the original admin’s ID in the session (typically session[:true_user_id]) and changes what current_user returns to the impersonated user. true_user gives you the original administrator’s object at any point. stop_impersonating_user clears that session variable and current_user reverts.
Security Notes
- Strict authorization: only trusted admins or support staff should reach these actions. Check the
before_actionfilters carefully. - Logging: log every impersonation event in production, who impersonated whom, when it started, and when it stopped. This is what makes it auditable rather than a backdoor.
- No password access: impersonation should never expose or let an admin change a user’s actual password. It’s for viewing, not for taking over the account.
- Session management: if you’re not using
pretender, be careful howtrue_userandimpersonated_userIDs are managed in the session. - Action Cable: if you use real-time features, verify that
current_usercontext carries over correctly into your Action Cable channels;pretenderhandles this well by default.
Impersonation is a genuinely useful addition to a Rails admin toolkit for debugging, support, and testing. The gem handles the mechanics; the security discipline around logging, authorization, and UI visibility is still on you.