Providing faster support using auto-impersonation
In the SaaS'es I usually work on (Flare, Mailcoach Cloud, Oh Dear), we've recently introduced an improvement to how we handle support. We call it "auto-impersonation".
In this blog post, I'd like to tell you all about it.
From impersonation...
When working on any Saas product, you'll get support questions from your users. Those questions will be specific to your SaaS but often involve something not working correctly in the UI or some weird results that are displayed.
You could check your database for those weird results or try to reproduce the UI bug in your local environment. Often, it is much easier to impersonate the user.
With "impersonation" we mean that you temporarily see your application as the user that needs support. Here's a concrete example. Let's imagine that Nuno has a support issue, and I want to see our application as Nuno would see it. In our admin panel, I can click the impersonation button next to Nuno's user.
Now I can see the entire application as if I'm logged in as Nuno.
This is super handy to see any weird state. I first learned of impersonation in the initial version of Laravel Spark, but I'm sure it has older origins.
...to auto-impersonation
What is better than impersonation? Auto-impersonation, of course! Let's discuss what we mean by that.
Imagine that Nuno files a support request using the little support bubble in the right button of the page (that form is powered by our support bubble package).
In our support mailbox, we get the message. A convenient feature of our support bubble package is that it sends the URL of the page where Nuno filed the support request. Notice that for Mailcoach, each of our user's teams has its own subdomain.
Without auto-impersonation, when we would click that link, we would have been greeted with a 404, because we should not be allowed to see Nuno's account. We should first head over to the admin panel, impersonate Nuno and then click the link.
Having to go to the admin panel gets old really quick, and we solved this by introducing auto-impersonation. If an admin is logged in, and a URL is requested that belongs to another account, we'll automatically impersonate the account's owner.
Auto impersonating isn't only handy for handling support issues but also for keeping an eye on the activity in your SaaS. Our SaaS logs important events to a Slack channel. Mailcoach Cloud is a relatively new product, so when new campaigns are sent, we like to keep an eye on how the campaign sending looks and feels for our users.
When somebody sends a new email campaign, we get a little notification in our Slack channel, with a link to view that campaign.
By clicking that link, as Mailcoach Cloud admins, we can immediately keep an eye on the sending process. That's super handy.
Implementing auto-impersonating
In the examples above from Mailcoach, you can see that each of our users has a separate subdomain. For each URL on those subdomains auto-impersonation works. Although we had good reasons for implementing it this way, I'm assuming that users won't have separate subdomains in most apps out there. So, can they not use auto-impersonation?
Sure they can! Let's look at another SaaS I regularly work on: Oh Dear. In that app, I use Laravel Nova as the admin panel, which has impersonation as a built-in feature.
Oh Dear does not use a subdomain for each user. Here are some URLs that we use at Oh Dear:
- https://ohdear.app/sites: displays all sites for the logged in user.
- https://ohdear.app/site/{site}/overview: displays an overview of all checks for a site
- https://ohdear.app/site/{site}/check/uptime: displays uptime results for the site
- https://ohdear.app/site/{site}/check/broken-links: displays broken links for the site
- https://ohdear.app/status-page/{statusPage}/overview: displays an overview of a status page
- ...
For the first URL in this list auto-impersonate will not work because there's no hint in the URL for which team the list should be displayed. For all other URLs, there's a {site}
or {statusPage}
in the URL. It resolves to a Site
or StatusPage
Eloquent model in that database. We can use that to determine the owning user.
In our HTTP kernel, we've added this AutoImpersonate
middleware. That middleware will detect if we should and can use auto-impersonation for the current request. We use Nova's ImpersonatesUsers
implementation to start impersonation when appropriate.
namespace App\Http\Middleware;
use App\Domain\Site\Models\Site;
use App\Domain\StatusPage\Models\StatusPage;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Laravel\Nova\Contracts\ImpersonatesUsers;
class AutoImpersonate
{
public function __construct(protected ImpersonatesUsers $impersonatesUsers)
{
}
public function handle($request, Closure $next)
{
/*
* If the logged in user is not allowed to impersonate someone,
* do not auto-impersonate
*/
if (! currentUser()->isAllowedToImpersonate()) {
return $next($request);
}
/** @var Site|StatusPage|null $primaryRouteModel */
$primaryRouteModel = $this->getPrimaryRouteModel($request);
/*
* If there was no Site or StatusPage found for the URL,
* do not auto-impersonate
*/
if (! $primaryRouteModel instanceof Model) {
return $next($request);
}
/*
* If the currently logged in user can already view this
* site or status page, do not auto-impersonate
*/
if (currentUser()->can('administer', $primaryRouteModel)) {
return $next($request);
}
/*
* Find the owner of the site or status page
*/
$owner = $primaryRouteModel->team->owner;
/*
* Add an entry to our audit log
*/
activity()->causedBy(currentUser())->on($owner)->log('impersonating');
/*
* Auto-impersonate the owner
*/
$guard = Auth::guard('web');
$this->impersonatesUsers->impersonate($request, $guard, $owner);
return $next($request);
}
protected function getPrimaryRouteModel($request): Site|StatusPage|null
{
if ($site = $request->route()->parameter('site')) {
return $site;
}
if ($statusPage = $request->route()->parameter('statusPage')) {
return $statusPage;
}
return null;
}
}
With this in place, we can just click a link like https://ohdear.app/sites/9421/check/broken-links/report in a support ticket and view that page.
Of course, we've also written tests around this functionality to ensure that the AutoImpersonate
works correctly, and only admins can use this. Any usage of this functionality is also written to our audit log.
In closing
Auto-impersonation can significantly speed up your support workflow. Though it might be dangerous for security reasons to automatically log in as someone else, I believe that it's an acceptable risk if you have good tests around this.
What are your thoughts on auto-impersonation? Let me know in the comments below.
From a technical standpoint, this is a really great and clean approach but from a legal perspective, your readers should be extremely careful about logging in as someone else without their explicitly approving it first, like maybe some kind of confirmation screen.
With all of the headaches around GDPR and data breaches lately, depending on the type of data an app contains (not just health records, but think about any kind of personally identifiable information/PII), you may be breaching your own ToS and/or you may be required to publicly disclose the fact that data was exposed to someone who the end user didn't sign off on.
You should absolutely consult your lawyer before implementing a feature like this.
You're right that security and focus on privacy is something that should be top of mind when implementing something like this.
We did check with legal experts and this is an acceptable way of going about it.
I've updated the screenshots of this post with the actual messages we are sending to Slack. They only contain id's and no extra information.
Also added to the post (it was previously omitted for brevity), is that we do log any usage of this feature.
Slightly off-topic, but I was wondering what you used to make the Mailcoach Cloud admin panel? Or is it custom made?
Since posting this question found out about Filament, pretty sure that's used here :)