Managing the Blog

Storing passwords securely

Summary

In this video, I show how to create user models for authentication in Go. I demonstrate how to create a user struct with fields for ID, timestamps, email, and hashed password. I build validation methods for passwords using bcrypt and implement a getUserByEmail function to retrieve users. I create a newUser function with proper validation and set up password confirmation to prevent typos. I use the Validator library to enforce rules like email format and minimum password length, add custom validation to ensure passwords match, and hash and pepper passwords for secure storage. I explain each step of the implementation while showing the code required to authenticate users, which prepares us for the next video on user login.

Transcript

Okay, so with that out of the way, we can now begin creating our models so we can store a user, we can hash and paper the password and validate it and do all of those things. So to begin, jump into models and create a new file called user.go in package models. Then let's create a struct called user that will have the ID field to be a UID. Create it at time to time. I also have an updated at time to time. Email is going to be a string. Email verified at is going to be a time to time. And we're just going to have a hashed password. It's going to be a string. Save. So this is basically how our user model is going to look like. Next, I want to create a method on this struct that's going to be called validate password. It's going to take in a password or let's call it provided password. It's going to be a string. And we are simply just going to be returning an error. And in here, we're going to be using bcrypt that we spoke about in the beginning. And from bcrypt, we're going to be using a function called compare hash and password. So first off, we're just going to be using the hashed password from the model. And am I in the correct CIM? And then you're going to pass another byte slice here. That is the provided password plus. And we are now going to grab the env variable. That's going to be called password pepper. And I have an error somewhere. This is correct. This should also be correct. I need another parenthesis. Give that a save. So this is just a function we're going to be using after we have pulled out the user to validate that the password we have provided is actually the one we expect. Next up, I'm going to create the hash and pepper password function. We have already seen this in the beginning of this module. So I'm just going to copy-paste it. And nothing really new here from the slide that we had. We simply take in a password or accept the password as a string. We add our password pepper to it, and then we generate from password using bcrypt. And we are just going to be using default cost. And if nothing goes wrong, then we have our hashed bytes here. This is literally just the hash in a byte-slice format that we then wrap in strings, so we can actually just store that as a string in our database. Then let's create another function here called getUserByEmail. And this one will return us either the user or an error. It will take in the context. It will take the dbtx, as we did with our articles. And then finally, it will take in the email, right? So let me just provide a little bit more space. And fairly straightforward, we just say user error, or let's call this user row error equals to db. And then we have our statements where we can say getUserByEmail. We're going to pass the context, the dbtx, and the email. We're going to return an empty user struct on an error, and then just return the error, and then deal with that higher up in the chain. But if nothing goes wrong, we can then return our user model. And we are just going to be doing the mapping from the database to how we represent a user in our application. So userRow.id. And we have the userRow.createdAt. Let's just copy this and update it to be updated at the email. Let's just use the one that we get from the database. And let's also pull out verifiedAt. And finally, userRow.password. There we go. So this is very similar and very similar to how we did articles as well. And now for a little bit more interesting part. We're going to be creating the logic to actually inserting a user. For that, we are going to be needing a struct called passwordConfirmation. It's going to have a password field. It's going to be a string and also a confirm password. So whenever we create a user, we want them to provide us with two passwords in case that they mistyped it. Again, our application will not have this user sign-off flow, but it's just good practices to always take in the password two times to ensure that it is what is expected. And then we're going to create a new user payload. It's going to be a struct. It's going to take in an email. It's going to be a string. And then the password is going to be the passwordConfirmation. Now, you also need to validate your data. And for that, I typically use a library called Validator. And what it allows us to do is just to add struct tags. So, for example, with our password here, we can say validate. And we're going to say required and GTE6 so that when running this through the validation function, it would guarantee that we have a field that is not a null string or null typed, and it is larger than six characters. And we have an error here. What did I do wrong? I need to wrap this in quotation marks. And we want the same thing for our confirm password. And then down here, we can say also validate. And we can say required so it's not an empty string. And it needs to be an email so that the package that we will pull in in just a second will check that the email that is provided is actually a valid email so we don't need to deal with the regex ourself. So this is the two structs that goes into creating a new user. Now, for the function, we're just going to create one here called new user, which, again, follows the same pattern as our get user by email. We will return the user or an error. And it will also take in context and the dbtx. And then we have our data, which is just our new user payload. There we go. Now, to validate, you could write all the logic yourself, but when pulling in the validator library, we can just say if error equals to validate struct, and then we pass the data. And if we have an error here, we can just return that error. Return an empty user struct. And we're going to wrap this in a join and say error domain validation and then the error as well. So we need to, of course, pull in validate in just a second, but we also need to create this error here. So let me just jump out again and say errors.go package models and say var and create a new error. Error is new. And let's just say the provided data did not pass validation. Give that a save. Now, whenever any of these validation rules that we have put on these two structs fail, we can then check, okay, is there a validation error on the domain level? And if there is, then we know better how to handle this because then we can jump into the errors that the library will return to us. But we just hope that everything goes well. Then let's just create a now and say user equals to and use a struct and say ID is equal to a new UUID. Create it. Add is now. Update it. Add now. The email is equal to data.email, and verified at is equal to now as well. Verified at. We do not have verified at. What did I call it? Email verified at. Email verified at. So far, so good. Now, the only thing we need is our hashed password. So let's say hashed password or error, and then use our hash and pair function. Text data.password.password. Again, if an error, we return it and deal with it up higher in the chain. And after all that, we can now say, let's say user row error equals to DB statements. Insert user past the context and DBTX as we always do. And then we have this insert params that we can fill out with our data. So the ID is going to be user ID. And now we are going to be using this PD type timestamp as we did with articles. Give that a save so we can do or get the input. We have the input right there. Good. Yeah, so let me just fill this in. So the time for created at is going to be user created at, and it is valid. So I can just a little bit here. We use it for our two other PD type timestamp. Just need to update data that we pass to it. There we go. And finally, we're going to say user email. Give that a save. Again, we're going to return the empty user struct and the error if there is any issues. If not, then we now have this newly created user row in our database that we can, for good measure, we can just return that, fill in or transform it like we did here with the get user by email. So let's just quickly do that. And this is all we're going to need for now, how to create users. So we validate the data is in a state that we expect. If it is, then we set up our user struct. We hash and paper the password, and then we pass that to our database that stores it, and then returns the user back to whoever called the function. So we're almost done now. We just need to get the validator package, and we also need to set up this validate as a variable so we can start referencing it in our models. So first of all, we're going to jump out back into our terminal. We're going to grab something from our notes here, which is literally just the go get for installing validator from the go-playground GitHub organization, right? We're going to be using version 10. So run that, get that added to our go.mod file, go into our models, and now we're going to create a new file called validator.go, package models, and we're going to expose this as a variable that is not exported. So validate, as we called it in our user's models file, and it's going to call a function here that's going to be called setupValidator, and this function I'm just going to grab from my notes is very simply just going to be a function that calls this validator.new because they do some caching, so we only need to set it up once. So now the first time this gets called, it gets set up, and we can then use the same instance every time we call validate. Then the case is simply just to add these struct validations. You can see we have an error here, validate passwords match, because we do not have it yet, but this function that we created in just a second gets applied to this struct. So what we really need to do is to create a function here called validateMatchPassword. It takes in a validator struct level, and in here we're going to say password pair or pbPair for short, slCurrent, and I'm going to call it interface, and it's going to be converted into password confirmation. There we go. Then we say pbPair.password does not equal pbPair.confirm password. Then we will report some errors. So let me just grab this here from my notes. So if our password does not match the confirm password, then the validator package will return an error with some information here. So if we just check the documentation, we can see that it reports the field name, the struct field name, a tag, and a param. So we have the field as an interface, we have the name of the field, and the struct field name is just the same. Finally, we have a tag, and then we don't really need a param. So this is also how you would create custom validations method using this validator package if you need to do something extra compared to just checking certain values of a specific field. Now, if you go back into our user, give it a save, we can see now that our error is gone. We are using this validator here, and we are now validating our struct data. We ensure that we have a valid email, that our password is longer than six characters, and that the password and confirm password match each other. So this is basically how you would create users, how you validate data, and how you would ensure that we store the data in a secure manner by hashing and peppering and soldering and doing all those kind of things. So now we are pretty much ready to start actually authenticating users, logging users in, which is what we're going to be focusing on in the next video.

Early Access

$95 $65 USD

Get access

Invoices and receipts available for easy company reimbursement