Introducing Simple Rails 8 Authentication
Let's say you're a Rails developer and you have an amazing idea for a new application. So you code, code, code, and you're left with something like this. This is an amazing task management application. You have your tasks, you can look at them, you can edit them and save them, mark them as complete, and everything looks amazing.
But I don't have any authentication with this app right now. All these tasks are just open for the whole world to see. But you know what? We're using Rails 8, so we can add authentication real quick. Let's do that right now.
Now we can just reload our app and wait, we have a sign-in page? What is this magic? This is the Rails 8's new authentication generator, and it is amazing. And in this episode, we're going to take a deep dive into it. So let's get into it.
Now, every single app needs authentication, but not every app needs the insane amount of authentication features provided by paid third parties like Auth0. I mean, my app is a task manager, and right now, I just need users to sign in and start using the thing. Then I'll be making thousands of dollars of that sweet, sweet SaaS money.
But in order to get there, we only need a simple solution, and this is all part of Rails 8's philosophy. Rails 8 is a batteries-included framework and allows you to go from Hello World to IPO. And now that we are still in the Hello World stage of our application here, we don't need the complexity that most gems, let alone paid third-party authentication solutions, bring about.
We are going to start simply, which is another core Rails philosophy, and handle this ourselves like the smart and good-looking developers we are. And here's a crazy secret. Authentication doesn't have to be hard or scary. And in this tech stack, we can roll our own authentication easily. And Rails 8 gives us the most amazing tools to do so. So let's get into it right now.
Generating Authentication and Exploring Models
So let's add authentication to our current Rails application. Now, right here we have a Rails 8 app, and it's pretty simple. It's a task management application, and we have the ability for a user to view tasks and edit them, make them complete, whatever. And it's pretty simple. But as of right now, we don't have authentication. So let's add that in.
Now, Rails 8 gives us the amazing feature of an authentication generator. All we have to do is run bin/rails generate authentication, and this makes a lot of code for us. So what happened? Well, let's just look at a couple of the files that we create here. You can see that in terms of our models, it creates a new session model and a new user model.
Now, these two are pretty self-explanatory. User has the normal things that you would expect a user to have. We have has_secure_password, which tells us that Rails is using bcrypt to encrypt the password, which is a thing that's been in Rails for a long time now, and it's a standard that is very, very good. And the user also has_many sessions, which means that whenever a user logs in, it's going to create a session object for that user and store it in the database, which I think is actually a very interesting design.
I think the reason behind this is so you can store your sessions in the database, so you can analyze the session data for any trends or anything you want to look out for with the sessions that users are creating. We can see here in the schema that our sessions have the IP address and the user agent of the user signing in, which is interesting information that you would want to store so you can analyze later.
Okay, so now we've run this generator, we want to make sure that our new models and their schemas are in the database. So we run bin/rails db:migrate. And now everything is up to date, the schema is up to date, and we're ready to look at the flow of our application with our new authentication.
The Authentication Flow Explained
But first, let's take a quick coffee break, shall we? That's smooth.
Anyway, so we've migrated our database, and we are going to restart our application, and now let's look at the new flow that we have with our authentication. We can refresh and see that we have a brand new sign-in page. Now, this doesn't look fantastic. Let me just update the styling real quick so we have something nice to look at. Okay, that looks much better.
So what exactly happens in our application now after we've generated our authentication? Well, let's look at the code really quick. So the user, as we can see from our routes file, will route to our tasks index page. But as we can see over here, we're being redirected to our sessions new page. Why does that happen? Well, let's look at our application controller really quick to get some clues. We can see here that the authentication generator created a new class that's included in our application controller called Authentication.
This class or module rather is a concern, and this concern is included in the application controller, which means that every single controller in our application gets the included code from this concern. And we can see on line 5 here that we created a before_action method that calls require_authentication. So when a user goes to the tasks index page, it's actually hitting require_authentication before it hits the index action. And what does require_authentication do? Well, it calls a couple of methods.
First, it calls resume_session, which looks in our current model, which, by the way, is a very special model that we're going to talk about in a second, and checks for the session attribute on current. If that's not available, it will find our session by cookie. Now, if resume_session doesn't equate to anything, what we do is we call request_authentication. Now, request_authentication, the very first thing it does is it stores the request URL into our current session, which is fantastic, so that whenever a user signs up, when they actually sign up, it will be redirected back to the URL that they're requesting initially, which in our case is tasks index. After that, it's stored in the session, we will redirect to our new session path.
So that is exactly what is happening in our new tasks controller with this authentication concern in our application controller. And that's why we're being redirected to our sessions new page.
Allowing Unauthenticated Access
But what if you want to skip authentication and allow anyone to see a certain page in your app? Well, that's pretty straightforward. You see the authentication concern actually gives us the allow_unauthenticated_access method. So let's go into our tasks controller here and see what we can do if we want to allow our tasks index action to be unauthenticated. What we can do is we can type allow_unauthenticated_access for only our index action. Let's just make it just for index. Now, if we write this file and reload our page, we can see that tasks are now just available without needing any authentication at all.
So it's really easy to allow certain actions in certain controllers to skip authentication if that is part of the logic of your application. Nice thing to have. Okay, now let's just delete this line. So we have our normal authentication logic, and I'm ready to sign in. But wait, I don't have any logins. Where are the logins that I need for signing in? Well, thankfully, Rails gives a nice fixtures file that it generates for you. So you can have some users that you can sign in with.
Signing In and the authenticate_by Method
So we can go to fixtures, users, and see that we have two users here, one and two, very aptly named. And we can see that the one has an email address of [email protected], and a password digest of what looks like an encrypted password based on the string password. Fantastic. Let's sign in with this user now, [email protected]. Password is our password. Don't steal that at anyone, okay? I'm keeping it in the app. And that didn't work. That's right. I need to load my fixtures. Let's do that really quickly. db:fixtures:load. Okay. Now, if we go back to our app, we refresh and we log in as [email protected] with the password of password.
We can see that we are now logged in. This is fantastic. So what happened when we logged in? Well, let's take a look at the flow one more time here. So if we go back to our authentication concern, what happens is we require_authentication, which calls resume_session, and this session is currently empty. So instead, we call request_authentication, which will redirect the user to our new sessions path.
So if we go to our sessions controller, which is generated again by our authentication generator, we can see that we have the authenticate action. This is what we submit to from the new form in the sessions controller, and we see it looks for the user with the authenticate_by method. Now, this has been in Active Record for a while now. It's part of the secure password module. And what authenticate_by does is really interesting. We can actually read the comments up here. It says, given a set of attributes, it will find a record using the non-password attributes, then it'll authenticate that record using the password attributes, which essentially means that we are going to find this user by our email address and then authenticate it by our password because the password is encrypted on this model.
Once we find and authenticate our user, we will call the start_new_session_for user method, which in our authentication concern is down here. We can see that this will create a new session for our user and store it in the database, which is what we showed off earlier. It will also store the user agent and IP address so that we can analyze them later if we need to. And it will add our session to our current model.
Introducing the Current Singleton Model
Well, that's interesting. What is our current model? Well, we haven't talked about that yet because it's a model, but it's also a special model. Let's look at it. Current is a special model that is a singleton in Rails. A singleton is a type of class that can only have one instance. So our current model only has one instance, which allows us to set global variables and access them throughout our application more easily. And we can see here in this model that we have an attribute of session and we delegate user to the session attribute. So anywhere in our app, we can just type current.user and get the current user based on the currently authenticated user, which is a fantastic pattern that, well, I'll show off right now.
Scoping Data with Current.user
You see, right now we have a little bit of a problem in our application. A user needs to be authenticated in order to see their tasks, but we show them all the tasks regardless. We can see in our tasks controller that the tasks index action actually just returns all of our tasks anyway. So it doesn't matter which user is logged in, they can see all the tasks regardless. So let's change that.
Now, the first thing I'm going to want to do is create another model so that we can actually scope our users and our tasks to one group in this application. So let's create a group model right now. bin/rails generate model Group and it will have a name, which is a string. And this model is basically a thing that's going to have many users and have many tasks so that when a user logs in, they will only be able to see the tasks that belong to their current group. And that will scope our controller really nicely here. So let's keep going. We generate our model.
Let's go to our migration file. Right within this migration here, I'm just going to make my life easier and add a couple of references for the users and the tasks. So now users and tasks will both belong to our groups. This migration actually could have been made a little bit simpler, even still, by just typing bin/rails generate model Group and add the name as a string. And instead of dropping those references into the actual migration file itself, we could have typed out user:references and task:references, and this will set up those relationships just like we did by dropping those lines into the migration file, but we could have all done it from the CLI here. So just keep that in mind for future reference.
So now let's drop our database and then recreate it, and then we can remigrate it. So now we have these new references and these new models, and everything is going to look really nice.
Setting Up Model Associations and Fixtures
So now we have this new model for groups, and we have these new has_many and belongs_to associations between the users and the groups and the tasks. Now let's continue. Really quickly, we can go to our task model and we can say that a task belongs to a group. Now we can go to our user model and say that the user also belongs to a group, but this is an optional belongs_to. Then we go to our group model and we want to make sure that a group has many tasks and has many users. Okay, great.
Now let's go to our group fixtures file. We can see that we have a couple of fixtures for groups. So this would be, let's just call it first group, and this will be second group. And so now in our tasks and users, we want to update these fixtures so that they can belong to a group. So this is going to be group is first group, and I want this one to also be in the first group, and the third task to be in the first group as well. But let's create a fourth task just to test out that we have our correct scoping on our groups. So second group for our fourth task.
Now let's go to our users' fixtures and make sure that our users belong to the correct groups as well. So these two first users can belong to the first group. Now let's make a third user, and this third user will belong to the second group. Okay, so we're just messing around with our fixture files, making sure our database looks nice, that when we log in, we can see a good bit of data that looks like it works correctly.
Implementing and Correcting Scoped Queries
So the very last thing we want to do here is actually scope our tasks controller to load only the current users' group's tasks. So instead of Task.all, we can type current, which is the current model. This gets set when it flows through the authentication concern. And current will have the user attribute, which is delegated from session. And from user, we can call group. And we want to all the group's tasks. So now whenever a user logs in, we will have this user's group's tasks. And this is important to scope it to the group and not just the user, because what if multiple users belong to the same group, you want to see all their tasks for that group. Anyways, let's just keep going.
So in our next task instance right here, we want to make sure that when we create a task, we're creating it for the current user's group as well. And there we go. So this is a really great way we can use this current model, this current singleton throughout our application. It just gives us a nice easy way to access this attribute on the currently authenticated user.
I'm actually realizing now I have a naming problem with my group. I named this relationship based on the group's name, but actually what I wanted to do was name it based on the attribute here, which is one. So I'm going to update my tasks to be group one. This is group one. This is group two. And I'm also going to update my users to make sure they have the correct associations as well. This is the first group, first group, and this is two. Okay, so now we can run bin/rails db:fixtures:load, and this will load all the fixtures into our database so we have some nice data that we can mess around with and see what we got here.
So let's log in as [email protected], and our password is password like I've shown before. Again, don't take that password. I'm going to production with this thing. Keep it quiet, okay? It's just between you and me. So now we can log in. And great. Now we have all of the tasks for this user's group.
Adding Logout and Verifying Scoping
But how do we know that? Well, let's log in as our third user, which belongs to a different group. But oh, wait, we can't log out right now. Well, thanks to the current model, it actually makes it really easy to log out. I'm just going to paste some code for a log out button. In our application.html.erb, we can have in our nav bar some log out functionality, which is really just a link to our sessions destroy endpoint, which again in our sessions controller destroy will terminate our session and redirect to the new session path.
And in this view, what we can do is we can use our current model to key off of our current user. If we have a current user, we want to log them out, else we want to display the button to log in. Great. So now let's refresh our page and we can log out. Now let's log in as our third user, [email protected], and the password, again, is password. And when we log in, we should see different tasks. And of course we do. We see different tasks because our fixtures for this user's group only has one task. So now we know that we have correctly scoped our tasks to the current user's group in the tasks controller. Fantastic. This is going great.
The Built-in Password Reset Flow
And if I log out, I can show off one more thing that Rails 8's authentication generator provides us, which is a nice forgot password flow. The forgot password flow is a fantastic thing that will allow a user to put in their email. They click on email reset instructions. It will send an email to them, generate a token and the user can reset their password.
Really quick, let's look at the passwords controller because this is the controller that handles all of that logic. We can see that under Create, if we have a user where we can find by the email address, we will reset this user and deliver a new password. And if we go into our password mailer reset HTML file, we can see that we link back to the password reset page for this user, and the link is the edit_password_url for this user.
Now, what is the edit_password_url? Well, I'm glad you asked. The edit_password_url is our edit passwords endpoint. And in this endpoint, we can see here that the user is going to be set by user token in this before_action here, only on edit and update. The user token is something that is created by Rails through our authentication generator. And when we try and update our password, we can see that the controller sets user by token, which is a special token that Rails uses to find this user to allow them to reset their password. So it's a nice straightforward flow that pretty much anyone would implement in any single application.
Why Sign-Up Isn't Included
Now, you might have noticed something. Signing up is not part of this generator. You can sign in, you can request a new password, but you cannot register and sign up. Now, you might wonder, why not? Well, this is a deliberate choice. You see, signing up is often something that is very tailored to an application. What's your sign-up flow? Do you need to make selections for a particular subscription level for the app? Do you have to require some information about the user instead of just the email and password? This generator isn't meant to solve all those needs.
It's just meant to solve the things that you would use in any given application. It's about giving you the foundation for the things that unlock authentication for you. And once the authentication flow is unlocked, the rest is up to you. You got this. So to sum this up, every app needs authentication, and Rails enables developers to have a straightforward, easy to understand authentication flow in seconds. And with Rails 8 giving you the power to go from Hello World to IPO, the authentication generator is a great way to get a basic but fully functional authentication feature in your application. If you want to see the rest of the series, then be sure to subscribe and come back. And hey, thanks, nerds.