Introduction to Secure Authentication
User authentication is crucial to every single website out there, but it can be difficult to build a secure user authentication system. So, in today's video, I'm going to walk you through all the cryptography and security steps you need to take to build your own login system using Node.js.
And if it's your first time around on this channel, make sure you subscribe for more videos where I simplify the web for you.
Project Initialization and Dependencies
To get started, I just have a blank project open, and the first thing we want to do is initialize it using npm. So, we can type in npm init, and if we put -y, it's just going to give us all the default values for a package.json. And there you go, you see it's created it for us.
And now, the next thing we need to do is actually install the packages we're going to use for creating our Express server. So, we need Express, which we're going to create our server with, and we're also going to need bcrypt, which is going to allow us to do all of our cryptography and securing of our passwords hashes.
After that's done installing, we're going to install another package, which is called nodemon, which will allow us to restart our server automatically without having to manually crash our server and then restart it. So, we can just type in npm i --save-dev nodemon, and this is just a dev dependency because we only are going to be using this when we're developing our site. And as I said, as anytime we make a change to our site, it's going to automatically refresh our server for us, so we don't have to do that manually.
And once that's done being downloaded, we're going to create a script that's going to allow us to start our server using nodemon. So, to do that, we can just come in here, create a script called devStart, and we just want to set that equal to here, nodemon, and we're going to call server.js, which is the file we're going to save our server into. And we can remove this test script since we're not going to use that.
Now, we can close out of this package.json and create that server.js file.
Setting Up the Express Server
Just like this. And in here, we actually want to set up Express. So, we're going to get Express first by saying const Express = require('Express'). This is going to pull in the Express library, and we want to get the app for that, so we're going to say const app = running that Express function. Next, we can just say app.listen on port 3000, and then just save that, and we can run npm run dev start. And that's going to start up our server on port 3000, but it's not going to do anything because we don't have any routes set up. So, let's create our very first route.
We're just going to create a route here, which is going to be app.get, and this is for getting all of our users. And when you create a real application, you're not going to want to have a route that exposes your users' password information, but for testing purposes and to show you how this works, we're going to create this users route. So, it's just going to be at /users, and it's going to come in here with the request and the response. And all we're going to do is we just want to send that users. So, we're going to say response.json, and we want to send our users.
So, let's create a users variable where we're going to store our users. In a real application, you would most likely want to store this in a database somewhere, but for testing purposes, just a local variable would be just fine.
Testing API Routes with a REST Client
Now, in order to actually test to make sure our API is working, let's create a file over here. We're just going to call it request.rest. And I'm using a package, which is over here, it's called Rest Client, and this allows me to make rest requests inside VS Code. If you want, you can use a package or application such as Postman to make these requests outside of your IDE text editor, but I prefer to do this in the text editor since it's easiest.
And what we want to do is we want to make a get request, which is going to be at localhost:3000. There we go. Make sure it says //, and we want to go 3000/users. And we can just send this request here, and you can see it's going to get an empty array of users because right now we have no users in our array. And if we wanted to add something, for example, we added a user with the name of name, and we rerun this, send the request, you'll see we get that user in our array. So, we know that this is working properly. Let's default this back to an empty array.
Building an Insecure User Creation Route
Now, with any form of user authentication system, we need to have a way to create users. So, we're just going to use a post request for that. We'll say app.post, and we're going to post to /users. Again, we're going to get that request and the response in here as our function. And in here, we need to do all of our code for creating a user, hashing the password that they sent to us, and saving it inside of this variable here.
Demonstrating the Plaintext Password Vulnerability
So, let's go over and emulate what our response is going to look like. We're just going to say POST here, http://localhost:3000. And we want to just post to users. And of course we want to make sure that the content type here is going to be for JSON, so we're going to say application/json. And essentially all we're going to do is we're going to pass a name. So, let's do a name here. We're going to say Kyle, for example. And then we want to pass a password as well. This is going to be in the password variable. And we're just going to pass along the password of password just for testing purposes. And essentially we're going to pass this to our server, and we want to convert this into a user in our users variable here.
Now the first thing you may be thinking is why not just put that directly into the users? So, we could just say request.body.name. So, we can get a variable here. User is going to be equal to the name just like this. And the password is going to be the same thing, request.body.password.
Flawed Implementation and Introduction to Hashing
And now you may be thinking that's perfectly fine. This is going to work. We can just say users.push(user). And we want to make sure that we're actually able to accept JSON, so we can just say app.use(express.json()) just like this. This will allow our application to accept JSON. And then we just want to say res.status. Status. Set that equal to 201, and just send a blank response back down to the user.
Now, if we come over here and test this, you're going to see it says it was created, sent us a blank response, and if we check all of our users, you see that our user is being saved here. But the problem is is our password here is stored in plain text. If anyone gets access to our database in any way, they have all of the passwords and usernames for every single user in our database. And we definitely don't want that. We want to make sure that our passwords are hashed so that even if someone gets access to our database, they won't actually know what the users' passwords are. This is where bcrypt comes in.
Understanding Hashing and Salting with bcrypt
Let's go back over to our server and require bcrypt. So, we're just going to create a variable here. bcrypt. And that's just going to be equal to require that library of bcrypt, just like this. And to hash a password, we need to have two steps. We need to number one, create a salt, and then we need to use that salt along with the password to create a hashed password.
And the purpose of the salt is if we hash a normal password, let's just say that we take the string here, password, we run it through some kind of algorithm, we'll just say a function called hash, just like this. And that is going to respond and return to us something. For example, let's just say it returns to us a password that or a hashed password that looks like this. Now, if we hash that exact same password later, it's going to return to us the exact same string. Which means if multiple users have the same password, they're going to have the exact same hash in our database. Which makes it easy if a potential malicious person gets access to our database and they crack one password, they're able to crack every other password that looks exactly the same and has the same hash.
So, the way a salt works is we hash our password, but what we do is we take some kind of salt and we add it to the beginning of our password before we hash it. And this salt is different for every single user. Which means that when we hash our password, it may look like this. And then if we come down here and hash a new password, we're going to use a different salt. And the hash for that password is going to look completely different, even though the passwords are exactly the same. This just makes it so that your database is more secure if someone gets access to it and they're not able to hash and break people's passwords because we have this salt. And we just need to make sure we store this salt along with the password. So, when the user tries to log in, we can use the same salt when we hash the password. And luckily, bcrypt takes care of all of this for us.
Implementing Secure Password Hashing
So, let's just delete all of this code here. And we actually want to use bcrypt, which is an asynchronous library. So, let's make sure we use an asynchronous function in here. And we're going to use a try-catch. And the first thing we want to do is we want to generate a salt. So, we're just going to say const salt = bcrypt.genSalt() and it's just not going to take any parameters. We can add in a rounds here. By default, this is going to be 10 and the larger you make this number, the longer it's going to take to make the hash, but the more secure it will be. So, for example, at 10, we can generate a few hashes per second, but if you bump this up to something like 20 or 30, it's going to take a few days to make one single hash. I just like to leave this at the default value, so just completely leave it out of there and it'll generate a salt for us.
And since this is an asynchronous function, we need to make sure that we await this and then we need to actually create our hashed password. So, we can say hashedPassword is going to be equal to again, this is an asynchronous function, so we're going to await it and we just want to say bcrypt.hash and this is just going to take in our normal password, which is request.body.password and then after that, it's going to take the salt that we want to append to our hash. And then we can just log that. So, let's do a console.log of our salt. And we're also going to console.log our hashed password.
And now we're going to bring all this code up here inside of our try. Just like that. And instead of saving our password as the normal plain text password, we're going to save this as our hashedPassword. Just like that. And the way bcrypt works is it's actually going to save the salt inside the password, so we don't need to separately say that we want to store the salt as well. It already has that information inside the hashed password. And then we just want to set a simple catch in here in case something goes wrong, we can just set the status here equal to 500. And we can just send down nothing.
Now, let's save that and test that out. If we go back over here, we click post, we're going to see that our salt is printed out right here and then we have our password, which is this entire thing down here. And you'll notice that the salt is at the very beginning of the password for every time that we encrypt it. And that's how bcrypt is able to use just the hashed password to be able to compare the other version because it saves both the salt, as you can see here at the beginning, as well as the hashed password. And if we run this again with the exact same password and we click send request, you'll see we get a different salt, which generates us a brand new different password at the end here. And now to test that even further, we can get a list of all of our users, and you can see we have that hashed password being stored. So, even if someone gets access to our database, they're not going to have direct access to the passwords, and they're going to have to crack them, which is incredibly time-consuming and difficult to do. So, most likely they won't be able to get any information from us.
Also, bcrypt has a nice way of doing both generating the salt and hashing the password in one single step. And we can just remove this salt section, and then here, instead of passing the salt, we pass the number of rounds we want, which by default is 10. So, we're just going to pass in 10 here, and that'll generate the salt for us without us having to do that first initial step. Let's remove these log statements, save it, and come back here and make sure everything's working. So, let's send a request to generate a user, and we'll get that user. And you can see it properly generated a user for us.
Creating the User Login Route
Now that we're able to store our user credentials, let's take a look at how we would log in a particular user. So, let's just copy all of this code up here, paste it down here, and essentially we're going to do a post request again, but we're going to post to users/login, and we want to make sure we just pass along the name and the password. Cuz we want to check the person's name, and then check to make sure that the password matches the password that they saved with us.
So, let's actually create that route here. Come down here, app.post, and we want to post to /users/login. And this is going to take in request and a response. And we're again going to make this an asynchronous function, because we're going to use bcrypt, which is an asynchronous library, to be able to compare our passwords. Now, the first thing we need to do is get our user. So, we can just create a variable here, which is user, and that's going to be equal to taking our users variable, and we're trying to find a particular user based on the name we passed in. So, if the user.name is equal to that we found a user from our initial list. This is just matching up the name, and we can come in here and put a single if statement. We just want to make sure that user actually exists. So, we'll say if the user is null, then we're going to send down an error to the user. We can just say return res.set status... Whoops, status. We want a 400 status and we want to send them down some text that says cannot find user. There we go.
Now, let's set up our try-catch because this is where we're actually going to do the comparison for our password. So, we can set up our catch and the catch is going to do the same thing as our catch in the other one, just return a 500 error and send nothing down to the user. And inside of our try, what we're going to use is we're going to use bcrypt.compare and we're going to first pass it the initial password, so request.body.password, and then we want to pass it the hashed password. So, we can just say user.password, which is our hashed version of password. And this is going to compare these two passwords. It's going to make sure to get the salt out of this, hash this initial password, and make sure that both the hashed versions equal the exact same thing.
Secure Password Comparison and Final Demo
And we need to make sure we do this with bcrypt.compare because it's going to be more secure because they're able to prevent timing attacks, which is a certain type of attack that you can get hit with if you don't use bcrypt.compare and constant timing algorithms, which just takes care of for you, so you don't even have to worry about it. Next, we just want to await this and we want to just check if these are the same, cuz it's going to return true or false for us.
So, if the password is the same, then we know our user's logged in. So, we can just say res.send('Success'). So, we know that they're logged in. And if for some reason this didn't work, these passwords are not the same, then we can just send something down here that says not allowed.
Now that we have that all saved, let's create a user. So, we can just send this post request and we can say here we have our user Kyle created and now let's try to log them in with a different password. This is a different password, so it should not work. And if we send this, you see we get an error saying that we're not allowed. But if we type in the correct password and click send, you see it's getting success because it's able to match these two passwords.
And that's all it takes to set up authentication. If you want to see a more in-depth tutorial of me creating a full application built around authentication, let me know down in the comments below and make sure to check out my other videos where I simplify the web, linked over here. Thank you very much for watching and have a good day.