Implement User Login in Remix with a Form Strategy and Session Storage

Ian Jones
InstructorIan Jones
Share this video with your friends

Social Share Links

Send Tweet
Published 2 years ago
Updated 2 years ago

Here comes the bulk of complexity when dealing with authentication. You need to handle verifying that a user account exists in the database and when it passes somehow referencing it throughout your application.

A great way to do this is through Session Storage. You can implement session storage with HTTP cookies (which is what you will do here) or with a database. You’ll implement functions commitSession, destroySession, and getSession but luckily the Remix docs will give you a head start here.

Along with session storage, you will need to implement an authentication strategy. Again there are a few ways you can do this whether that’s magic email, 3rd party auth like GitHub or Twitter, or email / password. Because you are already accepting email / password, that is what you will do!

Ian Jones: [0:00] To start off, you can fix the signup form by adding a submit button back. The user form now takes children. You can pass the button in this way.

[0:16] You can use the useTransition hook to adjust the text of the button, based on the state of the form.

[0:27] There are many ways to approach implementing authentication. For this project, you are going to use remix-auth and remix-auth-form. This is a simple way to add authentication without too much headache or signing up for another service.

[0:43] In your terminal, you can run npm install remix-auth and remix-auth-form.

[0:50] Back in your project code, you can add an auth.server.ts file. This file is where all your authentication code will live. This authenticator class comes from Remix Auth.

[1:03] It expects a parameter of session storage. This object will need to implement getSession, commitSession, and destroySession functions. You can implement sessions in a couple of different ways.

[1:18] This project will use HTTP cookies, but you can also create sessions with a database. To implement cookies session storage, head over to the Remix docs. You can see the basic example of how to create a cookie store. Too bad, these cookies aren't quite as tasty.

[1:37] Go ahead and add this code to a file called session.server.ts in your project code. There's a couple things that need to be modified for the cookie session store to work.

[1:54] First, you can update the name to remix social session. You won't be deploying to remix.run, so you need to change the domain the cookies live under.

[2:06] You can check the node mf, and if it's production, use the Vercel URL. If not, don't set a domain. This is important, because the browser won't recognize cookies from other domains.

[2:20] Just for this project, you can bump up expires and max age. For security reasons, you don't always want your cookies to be super long-lived. For this project, it's fine.

[2:33] This HttpOnly flag is important. It tells the browser that JavaScript should have access to this cookie. It's really the only way to keep cookies safe from someone else's JavaScript code.

[2:46] The last update is to make secure true, when you are in production. Secure makes the cookie only work on an HTTPS connection.

[2:56] Back in the auth file, you can import getSession, commitSession, and destroySession. These are the functions your authenticator needs to manage a session. You could implement these functions with the database instead, if you didn't want to use cookies.

[3:11] Now, it's time to dive into the meat of the login process. You will implement a form authentication strategy. There are many different authentication strategies to choose from, for example, an OAuth2 strategy, a magic sign-in link strategy, or even a GitHub login strategy.

[3:35] The form strategy is pretty straightforward. You have seen how to take an email and password to sign up for an account. Now, you're going to take those fields and log the user in. Just like any other form, you can pull the raw fields with form.get.

[3:54] Next, you pass those fields to the login schema. Notice that the parse function is being called. You want Zod to throw an error if the input is invalid, because you are handling the error in the loader function on your page.

[4:10] Finally, if the input is good, you can call the user_login() function.

[4:17] You will notice that the second argument you are parsing to the form strategy is user login. This is naming the form strategy, so that you can look it up later in a Remix action and pass the request to the strategy.

[4:33] Go ahead and create the login schema. The login schema will be exactly the same as the signup one.

[4:51] Now, it's time to create the user_login() function. Everything related to users is in user.server.ts, so you can add the function there. This function is long but don't be intimidated. User_login() takes an email and password.

[5:09] The first thing to do is to check if the email exist in the users table. If the user doesn't exist, you throw an error. If the user does exist, you can call verify_password() parsing in the hashed password from the user, and the plain text password from the form input you just got.

[5:29] The result of verify_password() will either be valid or invalid.

[5:33] If it's invalid, the passwords didn't match or something went wrong, so you throw an error. If it is valid, there's a possibility that the hash should be improved. You can update the hashed password in the database.

[5:49] After all that, you can return the user without a hashed password. Let's take a look at what verify_password() looks like. Again, you have quite a long function. Verify_password() takes a password and a hashed password.

[6:06] The return value of this function is a promise with an object of result that can be invalid or valid. There is optionally an error and optionally an improved hash. You have to convert these strings to a buffer. Remember that the hashed password was Base64 encoded.

[6:27] You have to pass that along to the buffer. Now, you have to handle the result from calling secure_password.verify(). If the result is valid, you return an object with the result being valid. Next, if the result is valid but needs rehashing, you can rehash the password with sp.hash.

[6:50] Finally, if the result is not valid, you can return invalid with an error. If any error was thrown in this process, you want to return invalid with the error to make sure you don't get a bad session. With user_login() and verify_password() implemented, you can wire up the login page.

[7:15] There are two major things you need to add to the login page, the Action for taking user input and calling the form strategy, and the loader for checking if the user is authenticated and grabbing any errors from the session.

[7:31] Inside of the Action, you can call authenticator.authenticate. You can pass the user login name as the first param. Next, the request. Finally, the config object for what you want the authenticator to do on success or failure.

[7:51] Optionally, you can pass throw on error to handle the error in the Action if you want. However, since failureRedirect is present, that will take precedence. The loader will check if the user is already authenticated. You don't want a user signing in again if they are.

[8:12] Next, you can get a reference to the session with getSession passing in the request cookies. Finally, you can see if the Action through an error by checking if the session has the session error key on it, returning a form error if there is.

[8:29] Down in the component, you can get the error with use loader data, and pass the error to the UserForm, and that's it. Now, you can create a user and login. I'll create a user with the email of ian@egghead.io and a password.

[8:48] When you sign in to the newly created user, and open up the developer tools on the application page, you can see that there's a cookie with the name of Remix social session. You just implemented user login with a form strategy. It's time to review.

[9:09] On the sign up page, you added an Action function that uses an authenticator object to login a user with the form strategy. If the user successfully signs in, they're redirected to the index route. If they don't, they're redirected back to the login page.

[9:28] The loader checks if the user is already logged in and directs them to the index page if they are. Next, you look up if there's an error on the session and pass it down to the UserForm if there is. For the authenticator, you're using cookie session storage, which you grab from the Remix Docs.

[9:49] You implemented a form strategy, parsing the raw input to make sure that you're getting good values. Then calling user_login(). Passing in email and password. Inside of user_login(), you check if the user with the email exists in the database. Next, you call verify_password(), passing in the password form value and the user's hashed password. If the result is invalid, you throw an error. If not, you can return the user.

[10:24] Inside of verify_password(), you call secure_password.verify(), converting the strings to a buffer. If the password is valid, you return valid. If it's not, you return invalid with an error.

ed leach
ed leach
~ a year ago

Request: I'd like a short course on the remix-auth authenticator.

Markdown supported.
Become a member to join the discussionEnroll Today