ActiveSupport::CurrentAttributes and do not fit the authentication Rails 8 generates.
Building it yourself turns out to be about thirty lines, because the generated code is small enough to read and structured to be extended.
What this covers
- Overriding
Current.userso impersonation flows through existing code. - Keeping
true_useravailable for authorization and display. - The ordering constraint in
resume_sessionthat will otherwise lock you out.
Requirements
How the generated code works
Two files matter.app/models/current.rb holds per-request state. It stores the session and delegates user to it:
app/controllers/concerns/authentication.rb assigns that session. require_authentication calls resume_session, which finds the session record from a signed cookie. When it returns nothing, the request is sent to the sign-in page.
The cookie is signed rather than encrypted, so a user can read their session ID but cannot change it.
Override Current.user
Everything in the application already asksCurrent.user who is signed in. Rather than teaching all of it about impersonation, intercept it in one place.
Replace the delegation with an explicit method:
Current.user now returns the impersonated user when there is one, and the signed-in user otherwise. With neither, it returns nil and the request is unauthenticated, which is what the rest of the concern expects.
true_user is not optional. You need it for the banner that tells an administrator who they are pretending to be, and you need it for authorization.
Add the impersonation methods
Inapp/controllers/concerns/authentication.rb:
Current is cleared after every request, so the impersonated user has to be found again on each one. The cookie session carries the ID between requests. The guard clause in find_impersonated_user avoids a query for a user that cannot exist.
Note what is deliberately absent: no Session record is created. Impersonation is not a sign-in. If you created one, the customer would see a second active session on their account, which is alarming and inaccurate.
Wire it into resume_session
stop_impersonating to terminate_session. Without it, signing out and back in resumes impersonating whoever you were impersonating before, because the ID is still in the cookie session.
Add the controller
Show it in the layout
Attributing impersonated activity
Impersonation creates a gap in your telemetry: every trace records the impersonated user, so support activity is indistinguishable from the customer’s own. Close it by tagging requests with the real actor:Transcript
Transcribed from the video and lightly edited: the automatic captions misheard a number of product and API names, and those have been corrected.Read the transcript
Read the transcript
0:02 Hey guys, this episode, we’re gonna be adding impersonation to the Rails 8 authentication generator. It’s actually a lot simpler than you might expect. So let’s dig in. I’m gonna run Rails generate authentication to create the user and the session models and all the controllers and the authentication module that takes care of most of the work. And we’re going to
rails db:migrate to create those database tables.0:31 Then I’m gonna run Rails console and we’re gonna create two users. We’ll log in as one, we will impersonate the other. So say user.create, bang, email, address. Chris@gorails.com and password is password. Then we’ll do the same thing, but we’ll make another user with a different email address.0:51 I’m gonna give it the old bah bah bah bah bah bah and we should be good to go. So let’s take a look at the authentication itself real fast. Let’s go into application@gmail.erb and we’ll say if authenticated question mark, then we can print out current user email address. So this is what we will see for the current logged in user and we wanna see this change when we’re impersonating a different user. Then we’ll also have a button to log out and this will go to the session path with method delete to log us out.1:34 Okay, so this is good. We should be able to run our Rails server and open this up in the browser. There’s my previous test that I had set up. And so here session new is where we’re gonna be taken. When we go to the root URL, it’s going to redirect us here.1:52 I’ve already created a main root URL for us. So it kicks us over to authenticate, which is what it will do by default. Every page will be authenticated and you have to explicitly tell it this doesn’t require authentication. So here we can go log in as chris@gorails.com and we’ll log in and there we go. Chris@gorails.com, I can log out and I can log back in.2:16 So we’ll do that once more and make sure that all works. So how do we do authentication with impersonation? Well, let’s take a look real quick at the current module or class. This is what handles storing the current session and the current user for the request. This will be cleared out after the request is finished and we just assign the session and we get the user through that session.2:44 So the authentication module is the one responsible for assigning that session. So here with require authentication, it’s going to then call this method require authentication, which will resume the current session or request that you log in with request authentication. Resuming is as simple as finding the session by cookie, which is going to look up the database session record based upon the ID set in your cookies. So these are signed so they can’t be tampered with but they’re accessible for users to read ‘cause it doesn’t matter, ‘cause they can’t do anything with that to abuse it. So for impersonation, we need to add some stuff to basically intercept this.3:29 And when we grab the current user, we actually get the user we’re impersonating, not the user we’re logged in as. And I don’t necessarily want to create a session for them either because we are not actually really logged in as them. We’re just trying to do some customer support, for example. So let’s talk about how we want this to work. In current.rb, this delegate user is actually what we need to intercept.3:54 So if we think about it this way, we could have an attribute for the impersonated user and we could assign this and then have a method for the user and we could then give it the impersonated user or the sessions user. And if there was no no session and there was no impersonated user, this would return nil and we wouldn’t be authenticated at all. So that would work pretty well and that would make it so that this also works with the authentication module. So we don’t really need that one line here for delegating the user ‘cause we’re gonna need to basically do that here and override that anyways. Now we could of course add a method like true user if we wanted to and say this is the sessions user and always be able to skip over the impersonated user ‘cause we want the impersonated user to kind of be like our current user but we might need to display a manner and show the original user which you could add a little helper for that here as well.5:01 So that’s optional, not necessarily something we need but let’s go into our application HTML, ERB and add a little thing here that says if impersonating, we want to have a button to stop impersonating. We’ll need some sort of impersonate path with the method of delete as well to stop the impersonation and then we can have a button to impersonate a user and this will go to the impersonate path but we would pass in like an ID for a user. This would probably need to be dynamic in your admin area and we’ll make this a post request because we want to create the impersonation session there. So we need to implement a handful of methods and a controller to pull this off. So let’s go to our routes.5:58 We’ll add a resource for impersonate. We can edit app controllers impersonates controller. To RB and create that impersonates controller and this will inherit from application controller. One thing we do want to do is add a require admin thing here. We don’t have an admin flag on our users but this is kind of an example that you could do.6:27 So you could say require admin and then this would be like current user admin and if you’re not a current user admin redirect to the root path or root URL, whatever you want to do. Maybe you want to put an alert there or something but in here we need to create our two actions. One will be to create the impersonation session and one will be to destroy it. This will be something like stop impersonating and we’ll redirect you to the root URL and this is for create is going to be pretty simple but we’re going to pretty similar. We’re going to say impersonate user find params ID.7:13 So you’ll pass in an ID in the URL. We will then impersonate that user and then redirect you to the homepage so you will now see the homepage as that user. So now we need to go into the authentication module and add those methods. So if we look at these other methods we can kind of build something similar to that. So we need the impersonating method which will return true or false.7:45 We need to have an impersonate user which is going to set that up. We need a stop impersonating. Doesn’t need anything there. For it to do but we also are going to need a fine impersonated user as well. So this is going to be the one that we use after you impersonate and you redirect to the next page.8:07 That request is going to need to find the impersonated user just like it finds your current session based upon the cookie. It’s going to need to do that. So let’s start simple. We’ll say current dot impersonated user is that present? Yes, you’re impersonating.8:28 No, you’re not. Then when we want to impersonate a user it’s assigning that variable, impersonated user equals that user. And we also need to set something so that impersonated user ID is kept around for the next request. So we’ll use this section for that. And when we are on the next request we can check and see is there an ID in the section for the impersonated user ID?8:58 If there is, we can say find by ID and try and get that user. Now the way I’m setting it up here is we’re checking to see if that ID is present and that way we can skip hitting Active Record at all if there is no ID. We don’t need to query the database for user where ID is null because we’re not going to get a user back for that. So there’s no point in even making that query at all. So we’ll either return the user or null for this and then stop impersonating is pretty simple.9:29 The impersonated user should be nil now and we want to session delete impersonated user. Pretty simple. So these are our main impersonation things. I’ll add another space here. I kind of noticed that that’s how the authentication module is already done.9:51 Some organization with extra new lines there. And so we need to wire this into the actual authentication process now. And the main authentication process is this method require authentication, either resume a session or restart a new one. And you send the user to the login page. So the resume a session is where we need to actually modify.10:10 So we’re going to have current impersonated user equal fine impersonated user. And then we’ll set the session. So these two are important that they’re in that specific order because the return value of resume session is what determines whether or not we redirect you to login. If you were to put this afterwards and you were not impersonating, then we’re just going to keep sending you to request authentication because impersonated user would be nil. This method would return nil here, which is falsey, which is then going to trigger this side.10:48 And you will seem to not ever be able to login. So that’s important that you put these correctly so that you have the return value of resume session set up there. And if all went well, the only other thing we really need is helper method for impersonating question mark so that we can use that in our views to display that button. Then you probably also want to check and see if the user is an admin or whatever as well. And then I just noticed we need to make sure we have user ID as the key here anytime we’re accessing the session.11:27 We want to make sure that’s consistent. Otherwise, you will run into weird things. Plus, when you’re terminating the session, we actually want to make sure we stop impersonating as well because when you log out, when you log back in, you don’t want to end up logging in as your account, but then you’re automatically impersonating whoever you were last impersonating. That would be weird. So we want to make sure that we stop impersonating when you log out as well.11:56 So let’s try this out. Let’s go to our browser. We are on sessions new. We can go log in, chris@gorails.com. That logs us in still, which is good.12:05 We haven’t broken anything. We are not impersonating so the impersonate button shows up. We can click this. It will make a post request to the impersonate controller that is going to hit create. It’s going to have that ID of two.12:20 We have it in the URL for this. You could also set this up on users/to/impersonate if you want. I just stuck them in one controller and you pass in the ID. Either way will work, but we can impersonate. And now, current.user.email address is Bob at Bob.Bob.12:37 But we are still logged in as chris@gorails.com. So we now set stop impersonating so it knows that we are impersonating. If we wanted to pull up the rails console, let me bump the fonts up. We can say session.count. And what we’ll see is there is only one session for me, chris@gorails.com.13:02 If we say session.first and we grab the user, it’s going to be user number one, which is I don’t show it here. It’s filtered, but email address as chris@gorails.com. So there’s only me logged in and my impersonation stuff is not being stored as a session, which would be weird if a customer saw that someone else is logged into their account. So we don’t really want to create a session for that. But you could if you wanted to.13:31 And also denote that on the record. You could add some extra field to the sessions model to keep track of those impersonation sessions for history as well. Lots that you can expand on this with. So now if we stop impersonating, this is going to set current impersonated user to nil, but also get rid of the session impersonated user ID. And we’ll be back to chris@gorails.com.13:58 And I can switch over to Bob at Bob.Bob anytime I want. And that really only took us modifying the resume session and the terminate session at the bottom to include our impersonation changes. And then our current.rb, instead of doing the delegation, we are going to use impersonated user. And of course, we always have access to that true user as well. So if we want to go into our application html.erb, we could say impersonating, you’re probably going to have the current user’s email address there.14:39 But maybe if you’re impersonating, you’d say current.true user.email address. If you wanted to show the real user that you have, now we’ll see chris@gorails.com show up in that spot. So you have access to both types of users, just like you would with the Pretender Gem for Manjukane or any of the other impersonation libraries that are out there for existing authentication. But they don’t work with current attributes. And so this is a nice thing that we can build ourselves with just a little bit of code.15:18 There’s not a whole lot here. And it’s easy enough to reason about as you look through this and figure out how is this working and so on. So I’m really happy with how this turns out. The Rails 8 authentication is very, very flexible. You can do all kinds of things with it and customize it to your heart’s content for whatever you need, whether it’s impersonation, OAuth, you name it.15:39 There’s all kinds of flexibility there. And it’s very, very easy to read with the authentication module here. So that is it for this episode. I hope you enjoyed it. And if you would like to see more stuff like this, let us know in the comments below.15:56 And we will use those for inspiration for our next lessons. That’s it. Have a good one. And I will talk to you later. Peace.