In 2018, it’s difficult to think of a mobile application that doesn’t do any communication with a server. Whether it’s analytics, storing your favorite memes or authenticating your users, chances are at some point your application will need to communicate with some kind of backend to create an experience that will delight your users. Fortunately, there are quite a few services that make it very easy to get up and running. But, while services like Firebase simplify basic tasks, you’ll quickly run into that one feature you need that Firebase doesn’t support. That’s where Vapor comes in. Vapor is a server-side development framework written in, and designed for, Swift. Built on top of Apple’s Swift NIO, Vapor delivers incredibly high performance through its non-blocking architecture, and its protocol-oriented design makes it very easy to learn.
In this tutorial, we’ll walk through the steps to configure a new Vapor 3 project, and then create the necessary functionality to authenticate (the process of verifying who you are) and authorize (the process of verifying that you have access to something) users using HTTP Basic Auth.
Creating the Project
As with any application, the first step is to create a new Vapor project. To do this, we’ll use one of Vapor’s many packages, the toolbox. Assuming you already have Vapor installed, open Terminal, navigate to your desired directory and enter the following (at the time of writing, the new project template for Vapor 3 has not been moved out of beta. In the near future,
--branch=beta may not be required):
vapor new MyProject --branch=beta
At this point, Vapor will start cloning its API template, and when it’s finished you should see an ASCII droplet in the Terminal window. To keep things as familiar as possible, we’ll now ask Vapor to generate an Xcode project that we can do the majority of our development in:
vapor xcode -y
When that is done, open up the newly created Xcode project and start to poke around- you should quickly notice a file at the root of your project called
Package.swift. This is similar to your Podfile (Cocoapods) or Cartfile (Carthage). It’s where you’ll designate your dependencies and define the individual targets in your application. In order to create our authentication capabilities, we’ll need to make some changes this file:
We’ll go into more details on the individual frameworks we’re going to use as we get to them, but we’ve now added Vapor’s SQLite database driver, authentication and crypto frameworks to our dependency list. Run
vapor xcode -y once more to ensure our Xcode project is up to date and our new dependencies are present.
At this point, we can build and run our project (remember to switch to the Run scheme in Xcode), but the output is pretty boring:
Server starting on http://localhost:8080
Modeling a User
In order to start building our authentication functionality, we need some way to represent a
User. This user will be given to us in a JSON representation upon registration and then we’ll need to store it somewhere so we can verify those same users on login (or authorization). Fortunately for us, Vapor makes this incredibly simple. Start by defining the
User struct as below:
There’s a few things going on here. First, we’ve declared a conformance to the
Content protocol. This is Vapor’s wrapper around
Codable (documentation here), allowing us to easily encode and decode the JSON we send and receive when routing HTTP requests. Next, we’ve declared a conformance to
SQLiteUUIDModel. This declares that we intend to store our
User object in an SQLite database, and we’ll use a
UUID as the object’s global identifier in the database (note: Vapor also supports
Int SQLite identifiers). Lastly, the
Migration conformance informs the database how it can represent this object in the event of a schema change.
There’s one last thing we need to do before we can use our
User object. We need to set up our database and inform it of the model objects it may need to migrate. Open
configure.swift and add the following:
There’s a few things going on here. First, we set up our routing engine and add that to the application’s services. Without this step, we’d be unable to interact with our backend. We then register the SQLite and Authentication providers and create our database. Lastly, we inform the migration of the model objects it needs to care about.
Now, if you build and run you should see the console report that some migrations have been completed. At this point, we’re ready to build a registration route and start persisting users.
Create a new file called
UserController.swift and add the following:
There is a lot going on here — let’s take it a few lines at a time.
- Implement our conformance to
RouteCollection. We’ll create a group of endpoints, and then create a new single POST endpoint at /api/users/register. The first parameter,
User.selfis simply a convenience from Vapor — it means that the body of the POST request should be able to be decoded into a
User. The function will
throwif that process fails (which in this case will case a 400 Bad Request).
- This part of the function is probably the most foreign, thanks to that wacky
Futuretype. There is way more to Futures than I could possibly hope to cover here- but at its core, it is Vapor’s way of dealing with asynchronous code. We take the user from our request and search our database. We want to find all
Userobjects whose e-mail is the same as the e-mail for the new user, and return the first one we find. Because the query and filter operations require an asynchronous search of our database (it’s very common for the database to reside in a different server than our HTTP routers, so that a single database can be shared amongst many instances of our HTTP routers), we’ll use a
Futureto indicate that our callback won’t be executed immediately.
- Check that an existing user does not exist. If it does, tell the user they made a mistake somewhere, because we don’t want multiple users with the same e-mail! Vapor is built heavily on Swift’s error handling functionality, meaning you can
throwfrom almost anywhere, and it will bubble up as an HTTP status code.
- We now want to prepare our new user for persistence. The first rule of authentication on the backend is to never save plaintext passwords. We’re going to use
BCryptto hash the password that was given to us before storing it. Using
BCryptwe can then verify a plaintext password attempt (for example on login) against our stored hash, but with the benefit of not storing the user’s actual plaintext password.
- Finally, we’ll create a new
Userobject with our modified password, and save it to our SQLite database (once again, this
saveoperation is asynchronous and returns a
Future. Assuming nothing goes wrong, we’ll return an
There’s one last thing we need to do before we can run our server — we need to register these new routes to the server so that they can be accessed. Head over to
routes.swift and add the following:
If you build and run, you should now be able to verify that your newly created registration route works. There are a number of great tools that can help with this (including Postman, Rested and even cURL), but I’m going to be using my personal favorite — Paw.
If you’ve done everything correctly so far, you’ll receive a 201 Created status code as a response from the server. If you did, congratulations! You just safely stored your first user. The final step is to allow those registered users to access some content that unregistered users can not.
Implementing HTTP Basic Auth
If you look back at the rest of the sample code that came with the starter project, you’ll see a
TodoController.swift which contains some code to manipulate Todos (I know, crazy right?). We want everyone to be able to fetch the current list of Todos, but only our registered users to be able to create new and delete old ones. So, how can we do that?
The first step is to conform our
User object to
BasicAuthenticatable. This conformance is very simple, it involves declaring a
WritableKeyPath for our username and for our password:
Finally, we’ll head back to
routes.swift and use another of the tools from
Vapor/Authentication and create an authorization middleware. At its core, a
Middleware is an object that runs a certain set of functions either before or after a route handler. This middleware could be used to log information about the request received, transform/modify any errors that are returned by the backend, or even, as in this case, ensure that the request is properly authorized to use the accessed endpoint. Go ahead and modify
routes.swift to include the following:
In this specific case, the
basicAuthMiddleware(using: BCrypt) is going to perform the actual validation of the Authorization headers. The
guardAuthMiddleware is going to ensure an error is thrown (and the appropriate HTTP status code returned) if that authorization fails.
Now, if you build and run your server, hitting the GET /todos endpoint should return a 200 OK and an array of Todo objects (it will probably be an empty list at this point — but still, you get the point).
But if you try to create a new To-do, you’ll get hit with an error:
If you’re familiar with basic authorization, the process involves sending your credentials to the server. You’ll need to create this credential (a String in the format: “<username>:<password>”) and then base-64 encode that string before attaching it to the ‘Authorization’ HTTP Header key. Once again, Paw makes this incredibly intuitive:
After creating this header value (and entering valid credentials) the 401s should cease and you can ask for the list of Todos and add new ones to your heart’s content!
This has been a whirlwind tour through authentication and authorization in Vapor 3. There is so much functionality in Vapor and we’ve barely dented the surface (entire books are being written about Vapor) — Futures and Databases are both fundamental to Vapor and when used correctly can do incredibly powerful things. To view the completed code to this project, check here (you may need to run
vapor xcode -y after downloading if you wish to use Xcode). To check out some of my other experiments with Vapor and iOS, check out my GitHub.