Episode #579

Create a User with a validated & hashed password

Series: Build a Vapor Backend

16 minutes
Published on June 3, 2024

This video is only available to subscribers. Get access to this video and 578 others.

This episode will introduce a new model for a User that will contain validations to ensure the email address format is correct (using a built-in regular expression) and that the password length is good and secure. We'll also ensure to hash the password with bcrypt before storing it in the database. Finally we'll make a custom Response model for our User so that we don't reveal internal fields to clients.

This episode uses Swift 5.10, Neovim 0.9.5, Vapor 4.92.

OK, so far in this series, we have implemented some APIs that are completely unprotected, and they are disconnected from any sort of user model or user authentication.

So we're going to fix that today.

We're going to start by creating a user model, and then we're going to add an API endpoint for creating the user, and then we can talk about authentication.

So let's go ahead and get started here.

We're going to go into our models folder, and I'm going to create a new model called user.swift.

I'm going to import vapor and fluent of final class user, which is a model and content, just like before.

We'll have a static let schema, and this will be users.

Then here we're going to have our ID with the key of ID.

Far ID is UUID, and this is very similar to what we've done already in our other models.

We're going to have an email field, and this is going to be both the email that we can use to contact the user as well as their username.

We're also going to have a field for the password hash.

We're not going to store plain text passwords, which you should never do, but instead we're going to hash the passwords and store that.

And it's worth noting that this password hash that we're going to be using is going to be a combination of both the hashed password and the salt.

So if you're familiar with password hashing strategies, this allows us to avoid a situation where a repeated hash of the same value gets the same result.

We can use a random salt.

Everybody gets a different salt, and so it limits the usefulness of getting a bunch of hashed passwords from a database.

So we're going to say password hash here as a string.

Then we'll have our timestamp fields.

So we'll have timestamp key is created at.

We're going to do that on create.

That will be created at date optional.

And then we'll have the same thing for updated at here and here.

That will be on update.

OK, we need to have an empty initializer to conform to model.

And we also need one that takes our properties like this.

So we'll say email string password hash is a string.

Then inside of here, we will assign those.

OK, so this is our user model.

It's very similar to what we've seen already.

The next thing I want to do is add an extension on user.

And we're going to create a struct called create payload.

That's going to be a content.

And it's also going to be validatable.

And this is a new protocol that we haven't seen yet.

This will allow us to validate the format of these params, similar to how we did with the before decode or after decode in the last episode.

So what we're going to do here is have all the properties that we expect the client to post to us.

So we're going to have an email address.

We're going to have a password.

And then we may also have a password confirmation just to ensure that the user has typed in the password correctly.

And then we're going to have a static func validations.

And these validations take an in out reference to this validation struct.

And so we can add validations to that.

So what we're going to do here is say validations.add.

And what we can do is use this overload here, where we add the key.

The key is going to be email.

Then we're going to specify the type that we want.

So it's going to be a string.self.

And here we can pass in is.

And there's validators that we can use.

And so there's validators like empty.

And you can even use not before it.

But there's actually one for email.

And what this is going to do is use a common and probably sufficient for most needs regular expression for parsing emails.

This is sometimes fraught and has issues where some invalid email addresses may get through or some validate email addresses may not.

So definitely double check the source code.

Take a look at the regular expression that it uses.

But it is pretty common.

The next thing we want to do is validate the password.

And the password is also going to be a string.

And at this point, we can say not.empty.

But really what we want is some sort of minimum and maximum length for a password.

And the reason why we want a minimum is pretty self-explanatory.

We want to make sure that people have secure passwords.

However, a maximum could prevent some sort of attack where, because we're going to be hashing these passwords, if somebody sends a million character password, it's going to tie up our server for a long time trying to hash those passwords.

And so that's an attack vector that people can use to take down your server.

And so here we can specify a range of characters, so between 8 and 1,000, which I think is pretty reasonable for a password.

And that is our create payload.

So we're going to be taking this in our controller.

So let's go over to our controllers.

I'm going to create a new users controller.

And it's going to be very similar to what we have here.

So I'm just going to copy that.

This will be users controller, users here, and here.

We're going to post users here and then users here as well.

So it'll be users post create.

This will take a sendable function.

And it will be called create.

It'll take the request.

It'll be async throws.

And it'll return a user.

OK, so the first thing we want to do here is validate the input.

So what I'm going to use is try user.create payload, which is the struct we just created.

And we're going to call the static function validate.

And we're going to pass in the content that's coming from the request.

So what will happen here is it's going to throw a validation error if any of those validation rules were violated.

And it's going to allow it to sort of early abort and ensure that what we do after this is going to be correct input.

So then we can say, OK, give me a payload.

This is going to be request content decode user.create payload.self.

We're going to guard that the password in the payload, payload.password, is equal to payload.password confirmation.

Otherwise, we need to abort this with a bad request and say that the reason is passwords did not match.

Now, this is another validation.

But currently, as of Vapor 4, there's no current way to validate multiple fields together.

So to say that this field equals this other field, I did experiment with this a bit.

And it seemed like I ran into some sendable issues if I try to add a validation that takes key paths.

So for now, I'm just going to do it in the controller.

Then we can create our user.

So we are going to say try user.

Then we're going to get the email address that comes from the payload.

And then now we need to add the password hash.

And Vapor comes with a bcrypt implementation with a hash function on it.

And we can hash the payload.password.

If you're curious, you can go to this module.

It's a Swift wrapper around a C implementation of bcrypt.

So it's worth a read to see what this is doing.

Particularly, I was interested in how this handles salting.

So as I mentioned before, we've got a hash plus a salt.

And that salt is random.

And the idea there, that gives you your digest or whatever you store in the database.

So that's sort of the idea there.

And this is all handled internally.

So the string that we get back is formatted in such a way that the hash is combined with the salt.

And so that's all we have to do.

We get a password hash.

We never store our plain text password.

And that is all good.

So once we have that, we can save the user.

So we're going to say user.save on request.db.

And then return the user.

OK, we are pretty close.

What are we missing here?

There we go.

We're missing a colon.

OK, and then we return the user.

So in order for us to try this out, we've got to do a couple of things.

Let's go to Configure.

And we're going to go down to where we have our migrations.

We're going to add another migration called create users.

And then we're going to go over to migrations.

We're going to say create users.

And we can just copy one of these.

Not there.

We'll go here.

Paste that.

So this is going to be create users.

And we will say users.

We've got an ID, a field of type email, a field of type password hash, which is a string and is required.

And then we've got timestamps.

And we're going to have some more things here.

But for now, this is good enough.

And then when we remove it, let's remove users.

We also want to make sure that we are unique on the email field, which means that two people can't occupy the same email address.

Now we have our migration ready.

And we've added that to the migrations list here.

The last thing we need to do is in our routes file-- let's just delete all the commented out stuff-- we're going to register the collection users controller.

So I've got the Docker database running here.

We're going to run-- let's do a Swift build and make sure everything is compiling.

OK, everything compiled.

Now we need to run app migrate in order to migrate our database, which is going to run the create users migration.

So I say yes.

Now that migration has been run.

Now we can run the app itself.

And I'm going to open up another pane here.

Let's give ourselves a little bit of room.

And then inside of here, we're going to say curl dash x post.

Then we can use the password asdf, password confirmation asd.

So we can test the error condition.

Then we also need to specify that this has a header of a content type of application JSON.

And then we're going to be doing that to local host 8080.

OK, so it can't find that because I need to put the actual route here.

So that's going to be users.

And at this point, it says it was not valid JSON.

Yep, I forgot a quote here.

OK, password is less than the minimum of eight characters, which is what I expected.

So let's do asdf asdf.

And now we should get a password doesn't match error, and we do.

And now we have created our user.

We can see that our user was created over here.

And it returns to us all this data.

And we really don't want to expose this to a client.

So I mentioned this before that we have control over what gets rendered.

So what we're going to do is create a custom response model so that we can always return protected data that we don't want to reveal to a client.

So let's go over to our user model.

Here we can return a response model.

And this is going to be content.

And then we're going to have an email address, which is a string, and then an ID, which is a UUID.

Those are the two things that we want to return to the client.

We're going to be taking in a user.

And we can say self.id equals user.

And we're going to use a require ID, which means that this needs a try and this needs a throws.

Then we can set the email address to email address like this.

Sorry, that needs to be user.email.

And then now that we have this, we can say that the user has a response model that returns a response.

And this is a throwing computed property, which is response with user self.

And that throws, so we need a try here.

OK, so now that we have the user.response model, this is going to be sort of our public response that we will return from our endpoints.

We're going to go back over to the user's controller.

And here, instead of returning a user, we're going to return a user.response.

And then this can be user.response.

And that can be a try.

OK, so let's go back over here.

We're going to rerun our server.

And then I'm going to run this again, which we're going to get an error saying that there's a unique constraint violation.

And this is actually a pretty good feature, that it's got a generic description to prevent leakage of sensitive data.

So this is basically saying that you may just want to have a generic, like something went wrong, rather than saying this email address is already taken.

But on the server side, we do know that this email address already exists.

So we had a unique key violation.

In our case, I'm just going to create another user, Ben 2.

And this time, we get just ID and email, which is what we wanted.

Now we're presenting just the information that is needed to the client and nothing more.

So that's a user model that we can use that included hashing of a password.

In the next episode, we're going to take a look at authentication.