Implementing Impersonation
Tags: rails, saas, webapp, security
Published: 2018-12-30 23:41:46

---
title: Implementing Impersonation
published: true
tags: rails, saas, webapp, security
canonical_url: https://jamie.ideasasylum.com/2018/09/29/implementing-impersonation/
---

The ability to log in as one of your users is one of the highest value features you can develop to support your customers.

The ability to log in as one of your users is one of the most _dangerous_ features you can develop to support your customers.

With that pithy introduction out of the way…

No, actually, let’s back up a minute because I’m not sure that you’ve fully appreciated what you’re about to do: **you are creating a security hole in your app**.

If your app is Helm’s Deep, then impersonating users is like adding a small unguarded culvert that bypasses the main fortifications. You should expect Orks… and add the appropriate defences.

![Helmsdeep](https://jamie.ideasasylum.com/images/helmsdeep.gif)

Still not afraid? Oh, maybe you’ve heard of Facebook? Yeah, this feature you’re about to blithely implement [resulted in 90 million compromised accounts](https://www.theregister.co.uk/2018/09/28/facebook_accounts_hacked_bug/) yesterday.

So, are you afraid _now_? Good. You may continue.

* * *

There’s a lot of things to consider when implementing a feature like this and the technical details are possibly the least interesting. They also vary considerably between apps, frameworks, and languages.

Technically, logging in as another user is probably as simple as `session[:current_user] = user.id` or something similar. Whatever. You probably know how this works.

Logging in as another user is not the hard part.

Here’s some more important things to consider:

- Do users need to give explicit permission for support staff to impersonate them?
- Who is authorised to impersonate users?
- How have you authenticated the support staff?
- How long does the impersonation last?
- How does the impersonator know they impersonating another user?
- What unintentional effects do you need to avoid?

## Getting permission

This might not be required in every application but if you’re dealing with sensitive or financial data you might need to ask the user’s permission before viewing their account. I’ve seen this implemented by FreeAgent as a special code visible in the user’s settings which must be provided directly to the support staff

![FreeAgent support access code](https://jamie.ideasasylum.com/images/freeagent-support-access.png)

The user can also opt-in/opt-out off allowing support staff to access their account.

## Who can impersonate users?

This is really an internal company process but you should be clear about who can and cannot impersonate a user, under what circumstances, and for what purposes. You probably want your support staff to impersonate users so they can fix/debug an ongoing issue. You probably don’t want your sales people impersonating users out of idle curiosity.

One feature I’ve previously built is some form of accountability. You might build an audit log in the database recording each time a member of the support staff impersonated a user. Personally, I think audit logs are great for analysing abuse _after_ it’s occurred but do little to act as a deterrent. Instead, I think good behaviour can be enforce by announcing the impersonation publicly — posting to a Slack channel each time some one is impersonated is a simple method of ensuring accountability.

## Authentication

Now that your admin accounts are a backdoor to every user account, it’s time to take another look at their security.

First, I think it’s important to have separate `Admin` and `User` models as the simplest way to avoid privilege escalation attacks

![You can't escalate a privilege if there's nothing to escalate](https://jamie.ideasasylum.com/images/you-can-t-escalate-privilege.jpg)

Next, we should ensure that it really is an admin impersonating the user. But don’t we just check that they’re logged-in as an admin?

Ha! Er… no. What happens if your support staff laptop is stolen? Or they’ve reused a password? You need another means of verifying it’s really an admin user. A sort of second password…a two-factor authentication if you will. 2FA. Top tip: just use [https://www.twilio.com/authy](https://www.twilio.com/authy) to generate and confirm a confirmation code. It’s dead simple and will take a few hours at most.

This ensures that the logged-in admin account is being operated by the member of staff you think it is.

## How long does the impersonation last?

A fairly common problem occurs when you impersonate a user on Friday, and then on Monday you open the app and forget you’re logged into that user’s account. Hopefully you realise in time before you do anything too… permanent like send a newsletter out with the wrong account.

A simple solution is to expire the impersonation much quicker than normal session cookies. If your user sessions normally last 30 days, I’d reduce the session timeout for impersonations to something like 1 hour.

## How does the admin know they impersonating another user?

Even if you limit the duration, you’ll still want to display some indication that they’re impersonating another user.

In one app, I added a large/prominent ghost 👻 fixed in the left-hand corner which would end the session when clicked. It was a fun but important feature. A banner at the top works just as well

![Impersonating a user in Podia](https://jamie.ideasasylum.com/images/podia-impersonation.png)

## What unintentional effects do you need to avoid?

It’s only after you’ve built an impersonation feature that you discover all the unintended side-effects. Try to shortcut this process by considering where else you send your user information.

Some of my hard-won lessons include:

- turn off Intercom when impersonating a user! Otherwise, you’ll send a message and end up it reading it yourself in the impersonated session… and the user will never get a notification! 
- disable all analytics or you’ll develop a very suspicious hotspot of user activity around your support staff’s location!
- if possible, disable user notifications/emails when an account is being impersonated — or remind staff that impersonating a user will still generate emails, notifications, and dashboard events.

## Multi-tenant applications

It’s slightly more complex to impersonate users when they’re on different subdomains or custom domains. The basic process isn’t too arduous though:

1. Generate a secure token attached to the target user’s account
2. Redirect the admin user to a special endpoint at the correct domain with the token as a parameter ([https://mycustomdomain.com/users/impersonate?token=ac8feb1b48fcddfe902814ff342de0d41e80d8d67e56d8182d634dbea1220e92f9dda4b0dbbe902ec460f119a435a684793e844b738529b42d6d60f12736b2f2](https://mycustomdomain.com/users/impersonate?token=ac8feb1b48fcddfe902814ff342de0d41e80d8d67e56d8182d634dbea1220e92f9dda4b0dbbe902ec460f119a435a684793e844b738529b42d6d60f12736b2f2))
3. Look up the target user account using the token
4. Sign them in using whatever version of `session[:current_user] = user.id` your app requires
5. Remove the token from the user account so the impersonation can’t be replayed

## Recap

So here’s the outline process for impersonating a user:

1. In your admin dashboard, let staff choose a user account to impersonate
2. Request a 2FA verification code to confirm the identity of the admin user
3. Once you’ve confirmed their identity, create the user session. In a simple web app, this might be just `session[:current_user] = user.id` . Or you might do the more complex multi-tenant dance with tokens and redirects.
4. Record the impersonation session in an audit log
5. Notify a team Slack channel with the details of the session
6. Add a session variable indicating that the account is being impersonated `session[:impersonating] = user.id` 
7. Display a banner with a warning message, the name current user, and a way to end the session
8. Disable all user analytics, both javascript and server-side
9. If necessary & possible, disable user notifications like account activity emails