Skip to main content

26 Devise Masquerade as another User

Episode 170 · January 31, 2017

Add masquerading (the ability to impersonate or login as another user) to your development or admin environments using the devise_masquerade gem

Authentication


Resources

<% if user_masquerade? %>
  <div class="alert alert-warning text-center" style="margin-bottom: 0px">
    You're logged in as <%= current_user.name %>.
    <%= link_to back_masquerade_path(current_user) do %>Logout <%= fa_icon "times" %><% end %>
  </div>
<% end %>

Transcripts

What's up guys, this episode we're going to talk about Devise Masquerading and how to add this gem so that you can log in as other users to make doing support, and finding and reproducing bugs a little bit easier. This is really useful in development, but it can also be very useful in production if you have the need to do some support and maybe log in to someone's account and try to do something and reproduce a bug or maybe just help them with their account, so we're going to talk about the devise masquerade gem, and then I'd like to do a follow up where we talk about how this is actually implemented behind the scenes, because it's actually rather interesting and not as complicated as you might think.

This gem is an extension for devise, so you're going to have to have devise installed in order to use it. It basically just hooks in, and you have a few methods that you can use, so you have devise :masqueradable and in your application controller, you'd put in a before_filter :masquerade_user!, and then you have your user_masquerade? methods and the ability for you to undo the masquerade afterwards. So this is pretty straight forwards, if you're used to devise, all of these methods are kind of familiar you use similar ones for current user and authenticate user, and in this case we're just doing masquerading instead.

We'll need some sort of an admin area in order to go use this, so we'll go set that up, and then we will take a look at this.

The site I've got here is an open source project that I've got, this is really straightforward, it's a link sharing app, you drop in a link and it will parse out the Open Graph tags and then it will show up on the site, and then people can vote on it if they're interested, and one of the things that I would like to do to the admin area is to add the ability for me to login as user, so I want to be able to click on a different user and click on a button up here to masquerade as that user so that I can login as them and test things out in development.

With that said, this will save me a lot of time in development so we can go add this gem in, and I'm going to pull the gem from the latest version on rubygems, so we will have that, we can then drop this into our Gemfile, and go to our console, and run the Rails server after running bundler, and once that has finished and restarted the Rails app, we can go add this

= link_to "Login As", masquerade_path(user)

into our Admin views. My admin is already scoped to admin's only, of course, and that's going to be important for this because you don't want avarage users to be able to log into someone else's account so keep that in mind. Make sure that if you implement this feature, it's either only available in development, or only available for admins, and it's probably a good idea to write tests for that to make sure that no non-admins can access it.

With that, we can then open up our app/views/admin/users/show.html.erb (create the show view if you don't have it), and in the header action section, this is where we can add a <%= link_to "Login As", masquerade_path(page.resource), class: "button" %>, and we can go back to the README and look at the other changes that we need to do, so for example, we need to go to the user model, and make it masqueradable

models/user.rb
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable, :masqueradable

application_controller.rb

    before_action :masquerade_user!

There is a couple helpers we can use to check to see if the user is masquerading, and to also reverse the masquerade, but let's see if we get the link in the admin area,

Small bug corrected on the link_to line, transcript already correct to save you some annoyance

If we open a new tab, you can see that I'm logged in as myself, Chris O, and if I login as this user, we are now logged in as test user, and refreshing our other tab we see that we're logged in as the test user, so that means it successfully masqueraded us as that user.

What it's doing behind the scenes is actually taking the user_id that you have, and you're currently logged in as, and it's moving that to a different place called "masquerade user ID's" so it knows who is doing the masquerading so you can undo it and go back to that user account, and since it moves that, and then it replaces the current user ID with the masqueraded user, so whoever you choose to login as, it makes that the current user id, and it checks for the presence of the masquerade user and that way, you can add a bar at the top saying "You are logged in as this user", which we're going to do right now, and then we can put a link in there, and it will check in both cases to see if you have masquerade id in your cookies, and if it does, it will display that header so that you can go and remove the current user, and take the masquerade user and put them back as the current user.

It's pretty straightforward what it's doing but they take care of everything for you and just give you a couple helpers and a couple links and voilà, you can build this feature into your app in like five minutes, it's really cool. What we need to do then is to go into our application.html.erb and at the top of the body, we want to put in our own nav, and this can be a new nav that is to contain our masqueraded user but we only show this nav stuff if the user is currently masquerading, so let's take a look at the helpers, they have user_masquerade, and we can wrap this with that

views/layouts/application.html.erb

<% if user_masquerade? %>
    <nav>
        <%= link_to "Reverse masquerade", back_masquerade_path(current_user) %>
    </nav>
<% end %>

We get an un-styled nav bar here at the top, that says "Reverse masquerade", and if we click that, we get back to "Chris O" as the user, so we go back to my own account and we're automatically set up, so this is really really seamless for us to be able to go do that. Now if we go back to the admin, you will notice that when we login as test_user again, this time if we try to ask for the admin access, we don't get any access to it, there's no route that matches, so we're truly logged in as the other user and the only way for us to know if we're logged in from a different account, is they save that cookie in the session, so we know that masqueraded user. So you could do some extra stuff if you wanted to, to set up the admin area so that it would use that masqueraded user account, but I wouldn't really recommend that because this is going to keep that really straightforward. No matter who you are logged into, you can only access the things that they have permissions for, and that's probably the best way to go about this. So we can paste in a little bit of CSS to clean up this navbar and make it look pretty, but as far as that goes, we just have to add in a link, and another link with a little wrapper around it to display this nav bar here, and we have a fully functioning masquerading feature in our app.

Some magic happens off camera

And here is the styled version of it! I made a quick little change to this, I added an alert style from bootstrap three and changed it to an alert warning, and that makes it all pretty, so at the top of the page I can simply click the "Logout" button, and I'm back to my current user account, and anytime we see that bar above the navbar we know that we are logged in and masquerading as a different user.

So that's as simple as that feature is, if you want to see the html that I wrote for that, I'll put that in the notes below and that is all you have to do to make devise_masquerade work.

To follow up on this episode and another one, I would like to build this from scratch so that you can see how this works behind the scenes but as you might have noticed, this is a devise specific solution, which works really well, but what if you rolled your own authentication or you're using something else like clearance. How do you go about building a generic solution for this that does the same thing just may not provide you the wonderful integration to devise as this does. So we'll talk about that in another episode and follow up this by building it from scratch. 'Till then, I will talk to you later, peace v

Transcript written by Miguel

Discussion


Gravatar
Matt Taylor on

Great video, can you explain more about the page.resources? where that is coming from and how it contains the user info for that method. Thanks

Gravatar

It's the resource for the current page that you're viewing inside Administrate. Just their naming convention since the admin is generic for any models you may have.

Gravatar
Matt Taylor on

Ah, got you. didn't connect the two when I was watching it.. thanks


Gravatar

Very cool! Looking forward to seeing how to build this from scratch!

Thanks, Chris!


Gravatar

Gravatar
Dan Frenette on

Awesome tutorial! Personally I found this plugin to be a much simpler alternative when adding this feature a few months ago, but both are very good nonetheless: https://github.com/ankane/p...

Gravatar

There's a bunch of great options like this. Cool thing about Pretender is that it can work with anything, but the nice part of devise_masquerade is it handles all the controllers and routes for you.


Gravatar

Great episode, will be nice to have another episode with JTW and **without** Devise.


Gravatar
Alex Musayev on

Authenticating as another user via admin console is a really nice idea. It may save a lot of time for QA. You inspired me to try something like this in one of my projects. But there is a bit different situation:

1. I have separate model for admin console users.
2. Admin console is running on a separate domain (this is the same Rails app with one common DB though).

Aparently in this case I'll have to implement custom solution instead of using devise_masquerade gem. Here's my idea:

- Authenticated admin clicks a link on admin console to sign is to primary application as some specific User.
- Application creates authentication token and saves it to DB. Something like this tuple: `AuthRequest.create(secret_token, target_user_id, token_expiration_time)` (assuming we have AuthRequest model to keep authentication requests).
- After token is persisted, admin console redirects the admin to primary application, using full URL with different domain name. `secret_token` should be one of the parameters for this request.
- Primary application validates secret token and authenticates current user with associated User records. Like so (the code is simplified):

``` ruby
class AuthRequestController
# Assuming this is an action that suppose to handle admin console redirects
def authenticate
auth_request = AuthRequest.find(param[:id])
sign_in(auth_request.target_user) # Calling Devise helper
auth_request.destroy! # Eliminating authentication request record
end
end
```

But may be there are more straightforward ways to do this. I'll be grateful if you share your opinion.

Gravatar
Alex Musayev on

(Forgot that Disqus ignores markdown, lol.)

Gravatar

That seems like a pretty decent solution cross-domain. Since you're sharing the database between the two, you can verify the token is only allowed for the user it was generated for and your expiration can be like 30 seconds so that the chance of that token leaking is very small.

You can also scope that AuthRequestsController to only allow admin users to access it as well so you get the same security around these tokens that devise masquerade does when it's only accessible from the admin.

Sounds like that'll work pretty nicely.

Gravatar
Alex Musayev on

Short timeouts make perfect sense. Thanks for reassuring me Chris! :)

Gravatar

You can atually use < pre >< code > tags for syntax highlighting :)
https://help.disqus.com/cus...


Gravatar

Hello Chris, I submitted a transcript for this episode, please review it so I can earn a free month. Thank you :)


Gravatar

Hi Chris, firstly, thanks a lot for your videos! They're so valuable!

My first question is related to using masquerade together with the friendly_id gem. I noticed masquerade_path(@user) is redirecting to /users/masquerade/chris, for instance. If I hardcode the user id - like in /users/masquerade/8 - it works. Any insight on how can I make it work properly?

Also, and even more important: if any user tries to open this URI, even if he's not an admin, he's able to access other users' accounts \o/ won't that happen in your application as well?

Gravatar

Since this is an administrative thing, you could explicitly pass in the user id like this: masquerade_path(@user.id) which should always put the numerical ID in, or you could take a look at overriding the masquerade query to use the friendly.find that is required for friendly_id lookups. I'd probably just pass in the ID explicitly since it's only accessible to admins.

And you can make sure this is accessible only for admins by doing this if you're using CanCan or putting your own before_action in the overridden controller to authorize for only admins: https://github.com/oivoodoo...

I don't think I mentioned authorizing that url in the episode like I should have. That's an important piece!


Login or create an account to join the conversation.