OAuth2 implementation with ORY Hydra, Vapor 3 and iOS 12
Part 2: User management in Vapor backend
Note: This post was originally written in 2018 and has not been updated since then. So this is not state of the art anymore, but maybe it can still be helpful to some people.
This part of our tutorial series about setting up OAuth2 with ORY Hydra, Vapor and iOS will focus on creating the Vapor backend. It will be taking care of user management and will also serve as the identity provider for our Hydra authorization server.
About Vapor
Vapor is a web framework that can be used to build server-side technologies like websites or APIs in Swift. It is built on top of Apple’s SwiftNIO, an event-driven, non-blocking network application framework.
Prerequisites
You should checkout the previous Part 1 of this tutorial series about setting up the ORY Hydra Server.
Create API project with Vapor 3
To get started with building our API server, you need to install Vapor on your computer. You can do this via homebrew for example. Checkout the Vapor Docs for more options.
Run the following command to install the latest version of Vapor:
brew install vapor/tap/vapor
Next we want to create the Vapor project for our API. To do so, run the following command:
vapor new auth-tutorial-api --template=api
The command vapor new {your project name} --template={choose a template}
takes two parameters:
{your project name}
: auth-tutorial-api in our case--template={choose a template}
: we use the API template. It generates some default code for an API with a Fluent ORM. Checkout the Vapor Docs for more templates
If you navigate to the newly created project you will notice, that there is no Xcode project created yet. We want to use Xcode as an editor for our Vapor project, but we are missing the project file. You can also use another editor of your choice. If you want to use Xcode, run the following command to generate the project:
vapor xcode
It will ask you if you want to open the project when it is created, just enter y
.
After opening the project in Xcode, you’ll see that Vapor magically created some dummy code for us, that we don’t need. Let’s remove that:
- delete the
TodoController.swift
file in the Controllers folder - delete the
Todo.swift
file in the Model folder - open
routes.swift
and delete everything inside of theroutes
function - open
configure.swift
and delete the line that adds a migration for theTodo
model:migrations.add(model: Todo.self, database: .sqlite)
We’ll have a look at the other generated code later to see what it does.
User management request handling
First we’ll focus on implementing the user management in our backend. If you remember the different components in our setup, this contains the endpoints for registering and logging in a user.
To provide the necessary endpoints on our API, we’ll need to do two things:
- create a controller to handle the incoming requests
- delegate route configuration to the controller in
routes.swift
Let’s start with creating the controller. Create a new file in Xcode called AuthController.swift
. In there we need to provide two functions that are called when requests on the two endpoints are received: register
for POST on /auth/register
and login
for POST on /auth/login
.
Both functions have similar signatures. They both receive the request that triggered the function call and the login data of the user who wants to login/register.
Now we want to make sure the router knows which functions to call when the endpoints are called. Open routes.swift
and add the following:
You’ll see an error telling you that AuthController
does not have any member called boot
. To fix that, go back to the AuthController
and add the following:
We make AuthController
conform to RouteCollection
and implement the required boot
function. Inside we link the endpoints to the functions that are supposed to be called. We also declare that whatever data is passed in the call of the endpoints should be decoded as aLoginPayload
.
You will still see two errors saying that the functions you provided do not conform to ResponseEncodable
. That means we need to return something that Vapor can encode to a response. We’ll get back to that later.
Registering a user
User model
Let’s implement the actual logic for registering a new user first. To create a new user, we need to create a User
model. Create a new file and add the following:
For now our user does not need a lot of properties. The only information we need is its email and password. Our user model needs to conform to three protocols. The first is the Content
protocol which inherits from ResponseEncodable
. This enables us to return User objects as a response in request handling functions like the ones in the controller. SQLiteModel
and Migration
are needed to be able to save users into an SQLite database.
As we want to use a database here, we should revisit configure.swift
.
Vapor already provided us with the necessary code to setup an SQLite database in memory. The only thing we need to add is a migration for our User
model. Add the following line before registering the migrations in the services:
Handling the register request
Now that we have our model and are able to save it in our database, we can implement the register function. Import Fluent
and modify the register
function to look like this:
First we need to check if we already have a user with the same email address in our database. If that is the case we want to respond with an error to let the user know that this email is already taken. Otherwise we can create a new User object and save it.
Input validation
Before saving the user into the database, we should make sure the user is providing a valid email address and password. For that, Vapor also provides convenience functionality via the Validatable
protocol. Let’s adjust our LoginPayload
accordingly.
To conform to Validatable
, LoginPayload
needs to implement the validations()
function. In there we define how email and password are supposed to be validated. Vapor provides email validation out of the box and passwords should be non-empty. Validation should be done before registering a new user, so let’s modify the register
function in our AuthController
.
Now we can be sure, we only write proper data to our database.
Another thing that comes to attention when reviewing the code is that it does not seem like a smart idea to write plain-text passwords into our database.
Password encryption
Vapor already provides convenience functions for encrypting sensitive data. Let’s do that for our password. Instead of just creating the user from the payload, we first encrypt the password and use that in our newly created user:
We added a new function to encrypt the password. To make this work, you’ll need to import Crypto
as well. Instead of just creating a new user model instance, we now call the function to create it with the encrypted password instead.
Response cleanup
One more thing we should consider is to not return the full user including the password in the response. That would be a big security issue. So what we should do is: create a new type called UserResponse
that only contains the id and email of the new user. Open User.swift
and add this at the end of the file:
Then change the register
function in the controller to the following:
We changed the return type to be Future<UserResponse>
and map the user to UserResponse
before returning it. That’s it. Registering a user is done. Nice 👍
User login
Next up is logging in a user. To do that we need to first check if we have a user with the provided email address in our database and then verify that the provided password is matching the password of our user. To do so, implement the login
function like this:
So, we got our login too. 😎
As we want to validate the input data before login and register, we should remove that duplication and create a new authenticate
function in the AuthController
, that takes care of that. Before doing that we’re going to rename the register
function to registerUser
though and the login
function to loginUser
. You’ll see why in a second.
The new function looks like this:
authenticate
validates the payload before doing a login or register. To differentiate between login and register, we added an enum called AuthType
. To make sure authenticate
gets called when the related endpoints are called, we need to re-add our route-handling login
and register
functions that now call authenticate
with the proper authTypes.
That’s all about user management we need to do for now! We’ll be back with Part 3, where we will implement the identity provider in the Vapor backend and extend the user management functions, so we can let Hydra know, when a user logged in. But first, let’s have a coffee break. ☕️
Further Reading
Vapor Documentation:
https://docs.vapor.codes/3.0/
Why you should use BCrypt to hash passwords:
https://medium.com/@danboterhoven/why-you-should-use-bcrypt-to-hash-passwords-af330100b861
User management with Vapor 3:
https://medium.com/rocket-fuel/basic-authentication-with-vapor-3-c074376256c3
Resources
You can find the fully implemented Vapor backend for this tutorial on Github!
Stay updated
If you liked this tutorial and are interested in further articles from us, follow us here or on Twitter and checkout our website!