Seeing Through Your Customers' Eyes with Customer Impersonation

Any website with customer support will eventually face an issue that is difficult for the customer to explain or difficult to understand without seeing what they are seeing. With the pretender gem we can log in to the users account and see exactly what they are seeing.

View the world through your customers' eyes
Trying to get support for an issue you're experiencing as a customer is often painful and slow, with the pain being compounded when the only way to contact support is via email. We can improve the experience by reducing the work the customer needs to do to fully articulate their issue. Without being able to see the issue a user is experiencing support agents must collect a lot of information and clarify every last detail to ensure they are understanding the customer correctly. We can reduce the amount of information required by giving the support agents the ability to log into the customers account.

In this article I will be using pretender to allow support agents to log in as other users. Pretender works with any authentication system - I will be using devise

Entering impersonation mode


First let's make sure we have all the gems defined in our Gemfile:

gem 'devise'
gem 'pundit'
gem 'pretender'

We also need to add a snippet into any controllers that we would like the customer support agents to be able to use while in impersonation mode. Let's add it to the ApplicationController so we can impersonate all areas of our application:

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  impersonates :user
end

We need a controller to allow support agents to log in and out of customer impersonation:

class CustomerImpersonationsController < ApplicationController
  before_action :authenticate_customer_support_agent!

  def create
    user = User.find!(params[:id])
    impersonate_user(user)
    redirect_to root_path
  end

  def destroy
    # current_user is the impersonated user before we call
    # stop_impersonating_user so we should build the notice here or store the
    # current_user in a variable so we can fetch values from it later
    notice = "You are logged out as #{current_user.email}"
    stop_impersonating_user
    redirect_to root_path, notice: notice
  end

  private def authenticate_customer_support_agent!
    # If we're already impersonating a user, true_user will be the customer
    # support agent
    #
    # If we're not currently impersonating a user, current_user will be the
    # customer support agent
    return if (true_user || current_user)&.customer_support_agent?

    raise ActionController::RoutingError, 'Not Found'
  end
end

Don't forget to add the routes:

Rails.application.routes.draw do
  resource :customer_impersonation, only: %i[create destroy]
end

The first step is opening the door

Now we need to add a form for the admins to sign in, we could do this in a couple of different ways.

1. Give the customer support agent a form where they can select and submit the user.
2. Create a button which uses the POST method to submit the users ID.

Most applications I've worked with have an admin page where you can view all of the registered users in a paginated table so we'll use go with option 2. It's really simple to implement thanks to Ruby on Rails' built in link_to method. We can tell the link_to method to use the POST method and then pass the user's ID to the path.

<table>
  <thead>
    <tr>
      <td>Name</td>
      <td>Email</td>
      <td></td>
    </tr>
  </thead>
  <tbody>
    <% @users.each do |user| %>
      <tr>
        <td><%= user.name %></td>
        <td><%= user.email %></td>
        <td>
          <%= link_to('Show', user) %>
          <%= link_to(
            'Impersonate',
            customer_impersonation_path(id: user.id),
            method: :post
          ) %>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

If you have multiple administrative roles you might want to consider creating a pundit policy to ensure only administrators with the correct roles are able to impersonate other users.

Now that we are able to impersonate a user we also need to be able to stop impersonating the user. We already have the route to do this and the controller action. We need to create a link for the impersonators so they can actually log out. We also need to remove the normal logout link otherwise the admin will be logged out of their account. To create a seamless UI, we can simply replace the existing logout link with the new stop impersonation link.

<% if user_signed_in?  %>
  <%= link_to(
    'Log out',
    true_user ? customer_impersonation_path : destroy_user_session_path,
    method: :delete
  ) %>
<% end %>

UI Considerations


There are a few things I highly recommend to protect your customers from accidental actions taken by your support agents.

Your customers likely expect privacy so you may wish to enact a policy of requesting permission to log into their account before allowing agents to do so. You could do this via chat, email, or create a system to request permission that is recorded in the application.

Keeping track of support protects you, your customers, and your support agents

I would also highly recommend logging all activity that happens while impersonating. Creating an audit trail is a good way to protect yourself from the inevitable accident that will happen in the future. With an audit trail you will be able to validate the correct action to take when the customer raises the issue. In our applications we go even further and log every request that a customer support agent is taking. Let's add that to the ApplicationController

bin/rails generate model AuditLog data:jsonb

class AuditLog < ApplicationModel
  belongs_to :user
end

class User
  has_many :audit_logs
end

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  after_action :log_impersonated_activity, if: :true_user
  impersonates :user

  private def log_impersonated_activity
    return unless true_user

    true_user.audit_logs.create!(
      data: {
        controller: params[:controller],
        action: params[:action],
        method: request.method,
        headers: headers,
        params: params,
        customer_id: current_user.id,
        customer_email: current_user.email,
        admin_id: true_user.id,
        admin_email: true_user.email
      }
    )
  end
end

I would also recommend restricting the actions the customer support agents can take. We typically use pundit for authorization, but unfortunately pundit isn't built in a way that expects two user objects. We can override the pundit_user to tell pundit where to find the user object. This means we can pass in a hash with the current_user and the true_user and update our ApplicationPolicy to reflect this change.

class ApplicationController
  private def pundit_user
    { current_user: current_user, true_user: true_user }
  end
end

class ApplicationPolicy
  attr_reader :user, :true_user, :record

  def initialize(current_and_true_user, record)
    @user = current_and_true_user[:current_user]
    @true_user = current_and_true_user[:true_user]
    @record = record
  end

  protected def impersonating_customer?
    user.present? && user != true_user
  end
end

Now in your other policies you can give different permissions while in impersonation mode. e.g. prevent customer support agents from making payments

class PaymentPolicy < ApplicationPolicy
  def create?
    !impersonating_customer?
  end
end

Making it obvious that caution is required will help reduce accidents

The last thing I think is important is making it very obvious that the support agent is in impersonation mode, and add some additional protections against accidental actions.

First we can add a warning banner to the page when a customer support agent is in impersonation mode

<% if true_user.present? %>
  <div class='banner danger warning'>
    You are signed in as <%= current_user.name %>.
    <%= link_to 'Stop impersonating', cutomer_impersonations_path(id: current_user.id), method: :delete %>
  </div>
<% end %>

Then we can add confirmations to all the important actions:

<%= f.submit 'Save', data: { confirm: "Are you sure you want to save this item for #{current_user.name}'s account" } %>

I recommend disabling any actions that should be protected:

<% if true_user %>
  <span class='disabled_impersonation'>Link account</span>
<% else %>
  <%= link_to 'Link account', link_account_path %>
<% end %>

That about wraps up this guide. We've touched on everything we need to allow our customer support agents to log into our customers accounts.

Ready to start your project? Contact Us

Like 4 likes
Joe Woodward
I'm Joe Woodward, a Ruby on Rails fanatic working with OOZOU in Bangkok, Thailand. I love Web Development, Software Design, Hardware Hacking.
Share:

Join the conversation

This will be shown public
All comments are moderated

Comments

Fizvlad
December 28th, 2021
There seems to be error in `ApplicationPolicy#impersonating_customer?`:
`true_user` is only blank when not signed in

Joe Woodward
January 14th, 2022
Hi Fizvlad,

It should be current_user != true_user. I've updated the article.

Thanks

Get our stories delivered

From us to your inbox weekly.