How To Masquerade As Another User To See How They Use Your App

Dan Croak

Recently, I created thoughtbot Foursquare lists to help out-of-town workshops students find hotels, coffee shops, and bars.

In the process, I noticed Foursquare has an “act as” feature:

foursquare masquerade

It lets you, well, “act as” another user:

foursquare masquerade

It caught my eye because I had recently implemented a similar feature for a client. I named it with slightly more aplomb: “masquerading.”

Ingredients

  • an authentication library
  • aplomb

Recipe

Here’s what we wanted:

Given a user exists with email "bobby@example.com" and name "Bobby Tables"
And an admin with email "admin@example.com"
When I sign in as "admin@example.com"
Then I should see "Bobby Tables"
When I follow "Masquerade" within the "bobby@example.com" row
And I should see "Now masquerading as Bobby Tables"
And I should see "Hi Bobby" within the navigation
When I follow "Stop Masquerading"
Then I should be on the admin page

The context is that I’m an admin. A user is on the phone with me right now with support questions. I quickly find their account and see the app through their eyes.

In app/views/admin/users/index.html.erb:

<% @users.each do |user| %>
    ...
    <%= link_to 'Masquerade', new_user_masquerade_path(user) %>
<% end %>

In config/routes.rb:

resources :users, only: [:edit, :update] do
  resources :masquerades, only: [:new]
end

Nothing crazy so far.

In app/controllers/masquerades_controller.rb:

class MasqueradesController < ApplicationController
  before_filter :authorize, :authorize_admin

  def new
    session[:admin_id] = current_user.id
    user = User.find(params[:user_id])
    sign_in(user)
    redirect_to home_path, notice: "Now masquerading as #{user.name}"
  end

  def destroy
    user = User.find(session[:admin_id])
    sign_in :user, user
    session[:admin_id] = nil
    redirect_to admin_users_path, notice: "Stopped masquerading"
  end
end

The masquerading controller actions are restricted to admins by before filters provided by the authentication library and the developer.

The create action switches the user ids and signs in as the user. Put your mask on because we’re masquerading.

When the customer support session is over, we’ll want to return to the admin views. In app/views/shared/_navigation.html.erb:

<% if masquerading? %>
  <%= link_to "Stop Masquerading", "#" %>
<% end %>

In application_controller.rb:

def masquerading?
  session[:admin_id].present?
end
helper_method :masquerading?

We define that in a controller, then expose it as a helper method to the views, so that we can alter the authorize_admin method that is used as a before_filter:

In application_controller.rb:

def authorize_admin
  current_user.admin? || masquerading?
end

That way, the MasqueradesController stays protected, even when you’re signed in as a non-admin user during a masquerade.

We aren’t using this technique on our own products right now. One concern is that on many apps, the customer’s view could reveal sensitive data. Airbrake, for example, would require at minimum asking the person “may I act as your account?”

Heroku’s ZenDesk support form does something similar: “May we access your application code? Check here to allow us to clone and/or inspect your code for debugging purposes.”

However, I like the idea of providing better support by viewing the app as our customers do.