SECRET OF CSS

Building the Rust Web App — Multiple Users and Authentication | by Garrett Udstrand | Jul, 2022


Creating a secure way to store passwords

1*5TKWeRNjMLwgtQVXCdMk0w
Photo by Thomas Ciszewski on Unsplash | Image height altered

This is the fifth part of a multi-part series about writing web apps. For this series, we will be writing the web app in Rust, and I explain to you how to write it yourself.

If you’d rather not write out the code yourself, however, I have made a repository with all the code written throughout this series here. I made a commit to the repository at the end of each part of the series. In the previous part, we gave our app more proper error handling and return values. In this part, we deal with the fact our app can’t handle multiple users.

As I mentioned way back, our app has a problem where it only properly works for one user. Or, at the very least, everyone sees everyone else’s tasks that they add. Obviously, we don’t want that. We want each user who uses the app to see their own content. In any app where a user adds their own data or edits their own set of data, we have to implement some sort of system to account for multiple users.

Since accounting for multiple users is a quite common problem, most frameworks have some built-in way to deal with it, and, even if yours doesn’t, there are a few common patterns for handling multiple users.

Rocket doesn’t have anything that specifically authenticates users, but it does have a feature we’ve been using called Request Guards, which allows us to validate or authenticate data before it comes in. We’ll be able to use that to ensure that a user is signed in before accessing or using certain routes.

To start, though, we need to create a users table in our database. Why? Because we’ll need to keep track of what users are using our app, and we need to keep track of their passwords so that, when they try to log in, we can verify that the person is who they say they are. And, whenever we need to hold onto or keep track of data in a web app, the first thought should be to create a table in the database for it.

You may wonder if storing passwords in our database is secure or safe. The short answer is yes. The long answer is that it depends on how you store the password. Many people do this wrong, so it can be insecure.

This article talks a lot about the problem of storing passwords. There’s also a greater argument to be made about whether every site should be implementing its own authentication. Maybe we should only have one authentication style for everything.

However, this is not an easy thing to implement, and whether we should even try to do something like that is not universally agreed upon. Just look at the discussion in this article and its comments. However, that article was written in 2006, and things like LastPass or 1Password basically solve this problem of login explosion.

In any case, the truth is that many web apps have their own authentication, so it’s useful to learn some of the basics of how it often works. With that settled, let’s go to our terminal and run the following:

sea-orm-cli migrate generate create_users_table

This will create a new migration that will create the table we store users in once we are done. But we first need to write some code to make that happen, so go to the newly created file in migration/src. We’re going to write code that will make a table called users with an id and two text columns: one to store the username and one to store the password. The code looks like this:

With that completed, let’s run cargo run to call this migration. After that, go to entity/src and create a file called users.rs. Then, enter the following:

Finally, go into entity/src/lib.rs and make it the following code:

pub mod tasks;
pub mod users;

With that, the table, the migration, and the entity for our users are all done. Now, we can work on implementing all the features we want related to users. As you can see, setting up the ORM was quite a pain, but now that we have it, it saves us a bunch of time in adding new tables and features. You can see how, if you have an app that may end up with hundreds of tables, this could be quite the boon.

In any case, we need to write a few things for our users. We need to have one request that returns a login page, one request that returns a sign-up page, a request that creates a user, and a request that logs in a user. Then, of course, after we’ve done all that, we need to use that Request Guard stuff actually to authenticate people, and we need to rewrite our current requests to only allow us to create, edit or delete stuff for specific users.

That’s a lot, but we’ll go through it step by step, and we’ll see that each step in the process is actually not that large.

First, let’s create the frontend for the signup page. Create a file called authorization.css and add the following code:

Next, over in container.css, add the following code:

* {
box-sizing: border-box;
}

Finally, create a file called signup_page.html.tera in templates and add the following code:

Now, in main.rs, we add the route to serve the template:

Of course, put signup_page in the routes.

The signup page relies on a post method that has createaccount as the route, so we have to discuss how we will store our users. For the most part, it will be just like storing tasks. They go into a table, and when we need some information from that table, we read from it. However, we need to be more secure with how we store users’ passwords because if someone gains access to our database, we don’t want them to be able to sign in to other people’s accounts easily.

We also don’t want these bad actors to determine what someone’s password is at all because people often use the same password for multiple websites.

Thus, when we store a user’s password, we don’t just store it in plain text. Instead, we use a hashing or encryption function to turn the password into a string of much less readable characters. The hope is that the hashing function is not reversible, so someone with a hash cannot determine what has input to create that hash. This means that the user’s password is safe for the most part, even if a bad actor gains access to our database.

How do we determine the right password is input if we have a random string of characters? We simply hash the input password and see if the hash of the input password matches the hash we have on hand. Thus, everything is kept secure, but people can still create and sign into accounts.

There is a lot more that goes into encryption and hashing and all the details around that, but for our purposes, we just need to know encryption functions exist and how we use them. For our project, we are going to be using a password-hashing function known as Argon2. Specifically, we’ll be using this implementation created for Rust. So, in the root Cargo.toml, put the following line under dependencies:

rust-argon2 = "1.0"

Now, at the top of main.rs, put the following line of code:

extern crate argon2;

Over in the users entity, add the following line of code:

pub const USER_PASSWORD_SALT: &[u8] = b"some_random_salt";

We need to have what is known as a “salt” for our hashing function to work. Ideally, this salt is kept secure and not put in a place where it will go into a public repository. So, know that me putting the salt here in the entity is not a great idea for a real project, but getting into the nuances of storing certain pieces of data securely for our app is a bit too much for this discussion now (if you’re curious how we’d keep this secret, we’d just put this in a file that doesn’t get put on GitHub.

Then, for any new developer joining the project, you’d share the salt with them over some secure messaging app. Of course, this isn’t the only way of keeping it secret/secure, and other, better methods exist, too).

Finally, add the following function to main.rs, as you can see below:

This takes in the username and password the user gives and uses our hashing function to store the new user. Of course, we have flashes and redirects to handle successes and errors. It should look a lot like add_task, because both are creation operations. This one just has a couple of extra bells and whistles.

Finally, mount this route in rocket like we’ve been doing for everything else. With that, we’ve made the signup page and its associated action.

Next is the login page! We’re going to jack most of the look and feel of the sign-up page, so we don’t even need to create a new CSS file. Just make login_page.html.tera in templates and enter the following code:

Now, in main.rs, let’s create the route that will display this page.

As you may have noticed, the login and signup pages are very similar, and we may be able to package up their similar functionality into functions or templates or whatever else. However, whether you do that is dependent on how much you want the two to actually look the same.

A lot of the code in this example is not perfect or amazingly clean, but it gets the job done. But, if you wish to clean up the code into something prettier, go ahead! It may be a good project to really think through everything being done.

In any case, our login page is now complete, so now we can implement the route that is going to verify our account

Now, the verification of the account shouldn’t be too bad. We use the username given to us to find the user in the database and verify that the password given matches the password in the database. Without any helper functions, we would just hash the password given and then compare the given password to the stored password.

However, the Argon2 library provides a method for verifying passwords, so we’ll be using that. In the case the password is not correct, we will use a flash to tell that to the user. If the password is correct, we set a cookie with the authenticated user’s id and use a flash to tell the user they have logged in successfully.

We’re going to need to add the secrets feature to Rocket to attach private cookies, though, so go to the dependencies.rocket block in your Cargo.toml and modify it to look like the following:

[dependencies.rocket]
version = "0.5.0-rc.2"
features = ["json", "secrets"]

With that out of the way, we can now add the following code to main.rs:

As you can see, most of our code goes into finding the user and returning appropriate errors when things go wrong. Only the bottom is really concerned with verifying the password. This is pretty messy code, and we’ll do a little work on cleaning this up once everything is finished, but we still have some more authentication things to do.

Now that we have the cookie set and our account verified, we will create a request guard to ensure our account is verified when entering certain routes. A request guard, in terms of Rocket, just allows us to perform some arbitrary verification on whatever request we attach it to.

Here, we’re going to verify that a user_id cookie is attached to the request.

This is not perfect. Ideally, the data stored in our cookie would tell us a lot more than the id of the user verified. It would also tell us when the user signed in, how long the cookie is valid and other details. Having that information on hand could allow us to make decisions to make our site more secure. Beyond that, if the cookie we set is more complicated than just a simple id, it will be harder to forge by bad actors. JSON Web Tokens, or JWT, do something exactly like that.

JWT works by taking a JSON object and encoding that into a string. Then, if a JWT were to come into the backend, we would decode the JWT back into JSON. That way, we can have all the information I just mentioned and anything else we want. Even better, this JWT can be signed to make it easier to trust its validity of it and encrypted to make it harder for bad actors to reverse engineer.

Thus, in an actual web application, you would probably use something like JWT for an authorization cookie rather than just using an ID. (However, I should mention that JWT’s are usually set in headers, not in a cookie)

Why am I not using something like JWT for this example? Mainly because it requires me to do a lot more work, the authorization is already a big hassle to add to this project, and I’ve nearly written 19,000 words about this web app and am starting to get quite fed up with adding extra features, especially since we’re so close to the end.

In any case, this is a toy example, so the code is a bit messy, and the choices I make are not really great for security. The more important point to take away is a lot of the choices I am making emulate how a lot of web apps are designed. While no competent web app would just set the user id as a cookie and use that for verification, the idea of a cookie being used to verify incoming requests is used quite a bit. It’s just that, instead of an id, the cookie will usually hold something like a JWT.

Regardless, let’s code the request guard. The first task is to import a library we should’ve imported much earlier, called anyhow. This is a library to make error handling in Rust a lot easier. Note, if you are going to write a library in Rust, it is worth the effort to make proper enums for your errors and to return those proper enums to a user.

But, when you are writing code for an application, using anyhow is generally a better idea to save on complexity and work. Especially because we don’t need as much detail as the proper enum errors provide most of the time. So, just add the following line to your Cargo.toml dependencies (we won’t actually end up using it extensively, but we’ll use it for the error type in our request guard)

anyhow = "1.0"

Now, here’s the code for the request guard:

Now, sadly we can’t just jump into applying the request guard to our requests. First, we have to update our tasks model and table to properly multiple users. Let’s talk about it

The whole point of adding users and authentication into our app was so that multiple people could use it. Obviously, if the app just adds and removes tasks from one table and displays every task in the table to the user, everyone would be seeing, editing, adding, and removing from the same set of tasks. We don’t want that.

We want users to see only the tasks they’ve added, and we want users only to be able to add, edit or remove their own tasks. However, as our tasks table is now, there is no realistic way to do that. There is no way to differentiate which user added a task to the table because we only store an id and the task itself in the table.

If you’re sharp, you probably figured out how we will differentiate them. We’ll add a user_id column to our tasks table, which will give us the id of the user that added the task. Then, we can figure out who added which tasks and thus can make the app work for multiple users. This user_id is called a foreign key, and relating tables to each other using foreign keys is one of the central concepts of relational databases. And, you guessed it, postgres is a relational database.

You don’t need to understand how the code actually creates a foreign key, just make sure the up function in your create_tasks_table migration looks like this

Of course, you’ll need to modify the Tasks enum in that same file to look like this to make it work:

#[derive(Iden)]
pub enum Tasks {
Table,
Id,
Item,
UserId
}

Next, go into migration/src/lib.rs and ensure that the create_users_table migration is listed before the create_tasks_table migration like the following:

With that setup, you can run the following command in your terminal in the project directory to modify your tasks table to now have a user id.

sea-orm-cli migrate fresh

Now, go to our tasks entity and modify it to look like the following:

Finally, go to your users entity and make sure it looks like the following:

With that sorted out, we can go back to main.rs and modify our CRUD operations.

With each CRUD operation, we want to modify them to use the request guard and to use the user_id returned from the request guard to add, remove, edit and view tasks from a specific user rather than just putting them in general. Also, if the request guard fails, we want our users to be redirected to our login page, which is where the extra routes with ranks come from. I’m just going to do all of them at once and put them into one large code block for your viewing pleasure, as this is starting to get quite tedious, and all of them will have very similar adjustments.

Of course, make sure all the routes are added to the rocket function.

Now, we just need to add the functionality to log out of our app, and we can put this whole painful authorization stuff behind us. Luckily, logging out is simple. We just have to remove the cookie that verifies us. But first, we need to add a button to our app that allows us to log out. So, go to todo_list.html.tera and add the following snippet right above the Todo List heading.

<form action="/logout" method="post">
<button type="submit">Log Out</button>
</form>

After that, go into main.rs and add the following code, making sure to register the route in rocket.

With that, you should now be able to use multiple users in the app. You should be able to create multiple accounts, log into them, make changes and find that the changes made in each account don’t affect other accounts. Also, if you try to maliciously go to any route that isn’t the login or signup page, you should be redirected away.

But that’s all I have for today. In the next and final part of this series, we’ll clean up the codebase and discuss where you can go from here, like what resources or projects to pursue to increase your web dev knowledge.

Thank you for reading this article. I hope this series will continue to help you improve your web development skills, even up to the end.



News Credit

%d bloggers like this: