OAuth2 implementation with ORY Hydra, Vapor 3 and iOS 12

Part 2: User management in Vapor backend

Hannah Teuteberg
12plus1
7 min readJul 30, 2021

--

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
Output of the ‘vapor new’ command

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.

Output of the ‘vapor xcode’ command

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 the routes function
  • open configure.swift and delete the line that adds a migration for the Todo model: migrations.add(model: Todo.self, database: .sqlite)
Remove all the stuff inside the red boxes

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.

AuthController.swift

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:

Registering AuthController in routes.swift

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:

boot() function in AuthController.swift

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:

User model

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.

SQLite database setup in 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:

User migration in configure.swift

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:

Register function in AuthController.swift

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 LoginPayloadaccordingly.

Let LoginPayload conform to Validatable

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.

Validate payload before registering a new user in AuthController.swift

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:

Register function in AuthController.swift

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:

User response struct in User.swift

Then change the register function in the controller to the following:

Register function in AuthController.swift

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:

Login function in AuthController.swift

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 function in AuthController.swift

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.

Final register and login functions in AuthController.swift

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!

--

--

Hannah Teuteberg
12plus1
0 Followers
Editor for

Co-Founder of 12plus1, iOS development, Swift