Complete Go for Professional Developers

Hashing & Storing User Passwords

Melkey
Twitch
Complete Go for Professional Developers

Lesson Description

The "Hashing & Storing User Passwords" Lesson is part of the full, Complete Go for Professional Developers course featured in this preview video. Here's what you'd learn in this lesson:

Melkey implements the password hashing function, which converts the plaintext password into a hashed version that can be stored in the database. The HandleRegisterUser function is updated to include the password hashing and appropriate error handling. The current code can be found in the 3.1 commit.

Preview
Close

Transcript from the "Hashing & Storing User Passwords" Lesson

>> Melkey: But now comes the how do we deal with their passwords, okay? And there's a very elegant way for us to deal with the password here, okay? First thing is we have to hash their password, everyone who see their password store them. A good rule of thumb is the less data you store, the better it is to maintain your app. The more PII, the more other stuff, then you have to kind of jump through all these hoops, regulations, and it just makes your job harder and tougher and more prone to attacks. But in our case, because we do wanna store user passwords, we're going to have to hash these. And so, how can we do this? Well, if we go back to our user_store.go file for just a second, you'll notice that our password is its own struct. And we know because a password is its own type or own struct, it can have its own methods. And this is a very powerful thing for us as we continue to create methods that a hash the passwords. And then when a user logs in, we can use that to see if the password they're passing is the plain text representation of what we have in our database. So here we can do p, it's gonna type password like so. Oops, and the first method we're gonna create is called set. I'm gonna take a plaintextPassword as a string and it's going to return an error, all right? And if you still kept bcrypt in you import now's a good time to remove that, ignore import, because here we're gonna do hash, err. I'm gonna write bcrypt, if I'm not mistaken, I think bcrypt only has a few methods we can do GenerateFromPassword, okay? And we're actually gonna transform that plaintextPassword into a slice of byte. So we can do slice byte, and we can pass in the plaintextPassword like so. And here would be the hashing algorithm, the salt. So I'm not gonna try to pretend I'm a security individual. The thing I know is the higher this value is, the more secure the hashing algorithm will be for hashing the password. However, the longer the compute time it takes to do and process that. The lower it is, the less secure that hash will be, but the faster it is at computing that hash representation from plaintextPassword. I think standard documentation shows 12 as a good middle ground. But again, this is depending on the need, depending on how it is, depending on what computer you have available. You can dial and tune this value accordingly. So here, if err != nil, we can just return that error like so. Otherwise, we can do be p.plainText is going to be plaintextPassword. And p.hash is going to be the hash like this, and then we can return nil. So we have one method to set the hash, we have to have another method that compares the hash representation. So if a user is logging in, that hash representation to a plain text, they could be submitting on the former. If you log in, you typically log in with a username and password. So you wanna make sure that that plain text representation of the password matches the hash that we stored in our database. So to do this, let's make another function, p.
>> Melkey: Password, and we're gonna call this function Matches like so. And it's gonna take a plaintextPassword which is gonna be a string, and then we're gonna have a bool and an error, like so. We're still gonna depend on bcrypt, so we can create this error. And I'm gonna do bcrypt.CompareHashAndPassword. So here we're gonna (p.hash), okay, to again, the byte representation of plaintextPassword like so. Oops, if err != nil. There're actually a few errors that could happen, one is that CompareHashAndPassword does throw an error if they mismatch, okay? So in this case we're gonna add a switch statement and here case errors.Is okay? So if the error from calling bcrypt matches the error from bcrypt and its Err, where is it, MismatchedHashAndPassword, okay? So if that's the case, we're gonna return false and nil. Meaning false that the passwords do not match, but there's no true error, okay? Bcrypt just throws an error if it doesn't matches, okay? Otherwise, we're gonna have our default case. So if error does not equal to nil and is the error MismatchedHashAndPassword err, what we're gonna do is default, return false and err, okay? So this is something like an internal server error, right? Internal server error, like something's going on. Otherwise, if anything is great, we can return true and nill. Now, if we go back to our user_handler and we are underneath this part with how do we deal with their passwords, we can now make use of those password functions we just created. So here we can do err, we can put user.PasswordHash.Set, okay? We can put in (req.Password), like so. Next, let's check the error. So if error != nil, what we can do is h.logger and we can just do Printf not fatal, we can do ("ERROR").
>> Melkey: You can say hashing password, again, this is going to be for us. So %V, err, whereas we'll respond to our client. We can use utils.WriteJSON(w, ) we can do http.StatusInternalServerError, okay? And this part gets kinda dicey, because, like I showed you earlier, well, if we're setting this not a problem. But for when we do matches, you don't wanna tell a potential bad actor, like a fraud actually try to hack an account, that combination of username exists and the password's wrong, right? Then they know, okay, so there's actually a user with that username or email in your database, I just have to crack the password. So again, my expertise is definitely not security or how to handle that. I'll refer to some experts to give the best suggestions. But when you do respond to your client, you don't explicitly say, hey, password is not correct. You just kinda give a broad statement, say, we can't match the two combinations of using our password, for example, right? But again, I'll refer to experts in the field on what they suggest for handling that. But in this case, because we know we are just setting the password, we know that we can do error, "internal server error", and we can just return.
>> Melkey: All right, now, with all of that being done, we can finally call the function that creates the user, so here we can do err. We can call h.userStore.CreateUser, and we can pass in that user like so. If err != nil go ahead and just copy the logger and the Write.JSON with the return. So instead of hashing password, we can make registering user, just notice the typo up here.
>> Melkey: And you can keep it like that. And while you copy that down, we're gonna do that. If there is no error, we're able to successfully create the user. We can do w, http.StatusOK, and we can also send back utils.Envelope, we can say "user", and bring that user like so. And actually, it's not StatusOK, StatusCreated is the more appropriate status response here. We did the database layer, we did the API layer. We just have to do one last layer, which is the easiest, and that's routing it across our application, which we know how to do. We first should start in the app.go like so. And now, since we've introduced a new store, we can go all the way up here, okay? Let's first create the new UserHandler *api.UserHandler, like so. And then right here, we can do userStore := store.NewPostgressUserStore, pass (pgDB). And in here, userHandler := api.NewUserHandler. We'll pass in the userStore and the logger, like so, and then underneath UserHandler, we'll pass in the userHandler. Last thing we got to do is, go into our routes. And let's do r.Post, this is gonna be "/users", and here app.UserHandle.HandleRegisterUser, all right? So now we have fully connected everything. Again, we do this one much faster. We've connected our user database to our user API, across our app, through our routes. Only thing left is to test it. So we have this HandledRegisterUser, go to your running go server, you can nuke that, run go run main.go, see if you have any obvious errors. I do not, that's good. Make sure you Docker is still running, okay?
>> Melkey: Check, select all.
>> Melkey: Okay, if you've got that issue, I'm just double checking that just to confirm I don't have any users in my database, select all from users, psql shows me it is empty. And if I just go back to the post_notes.txt. If I go down to section 3.1 right here, I've provided an example payload request for you to copy and paste into your terminal or into your URL application, or app API application. So paste this, you can see here it's curl -X POST, which is the method. Localhost:8080/users, the application type, and then the payload, which includes a username, email, password and bio. So click enter, you can now see we've gotten a response, "user", "id": 1, "username", "email", "bio", "created_at" and "updated_at". And now if I go to psql and run the exact same command to check if we have a user. You now should see we do, in fact, have our username, John Doe, with all of their credentials. And you can notice the password is, in fact, hashed.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now