Vapor 3 Series II — Authentication
In the previous article, we finished a simple RESTful API server with Vapor 3. More specifically, we created our User
model to store data into an in-memory SQLite database and implemented our UsersController
to handle interactions from a client. Although our server has many great features already, it also has one problem: anyone can create new users and delete them. In other words, there is no authentication on the endpoints to ensure that only known users can manipulate the database. In this article, I am going to demonstrate how to store passwords and authenticated users, and how to protect our endpoints with HTTP basic and bearer token authentications.
Please notice that this article will base on the previous implementation.
User Password
Generally speaking, authentication is the process of verifying who someone is, and one common way to authenticate users is using the username and password. Open our User.swift
file and add the following property below var username: String
.
Next, we should replace the initializer to store the new property with the following.
Since our User
model already conforms Content
protocol, we don't have to make any additional changes to create users with passwords.
However, instead of storing passwords in plain text, we should always store passwords in a secure way. Fortunately, Vapor provides BCrypt
to hash passwords, and we can take the advantage of this to secure passwords. Switch to UsersController.swift
file, and append the following line below import Vapor
.
This import statement allows us to use BCrypt
, and we can use it within createHandler
and updateHandler
methods. Let's replace these two methods with the following implementation.
These two new methods will hash the user’s password before saving it into the database.
At this point, if we try to create one user and retrieve afterward with Postman, we can see that the response contains the password hash. However, we should protect password hashes and never return them in responses. In order to achieve this, go back to User
model and write the following lines below User
's new initializer.
This creates an inner class to represent a public view of User
. Next, add the following line under extension User: Content {}
.
This line allows us to return this public view in responses. Moreover, let’s write some helper methods for User
and Future
to keep our UsersController
concise. Please append the following extensions at the bottom of our User
model.
These two new methods help us to change our router handlers to return the public view. Finally, within our UsersController
, change our handler methods to return the public view.
Now, any calls to our endpoints to retrieve a user won’t return a password hash.
Basic Authentication
Vapor already has a package to help with handling many types of authentication, including HTTP basic authentication. Open Package.swift
and replace .package(url: "https://github.com/vapor/fluent-sqlite.git", from: "3.0.0")
with the following lines.
This adds the authentication package as a dependency to our project. Furthermore, change the dependencies array for the App
target to the following.
This adds the Authentication
module as a dependency to the App
target. Then, please regenerate the Xcode project to install the new dependency with vapor xcode -y
. Before modifying our User
model, we have to change configure.swift
file to adopt the new dependency. Go to configure.swift
file and append the following line below import Vapor
.
Additionally, add the following line under try services.register(FluentSQLiteProvider())
as well.
This line registers the necessary service with our application to make sure authentication works.
Let’s start to use the new dependency in our User
model. First of all, append the following line below import FluentSQLite
.
After that, we are able to adopt HTTP basic authentication with the Authentication
module. Secondly, please add the following lines at the bottom of our User
model.
This extension tells Vapor which property of our User
model the username is and which one the password is. Since HTTP basic authentication uses the username and password to identify users, we should prevent multiple users from having the same username. Let's replace extension User: Migration {}
with the following lines.
This custom migration will add all the columns to the User
table using User
's properties, and more importantly add a unique index to username
on User
.
Next, switch to our UsersController
and add the following lines at the bottom of boot(router:)
method.
Here we use two middlewares to ensure that requests of creating a user contain a valid authorization, otherwise an error will be thrown. Moreover, inside boot(router:)
method, please remove the following line as well.
At this point, if we run the application and try to create a new user with Postman, we are going to receive a 401 Unauthorized
error response. Since we are using an in-memory database, there is no existing user in the database every time we run the application. As a result, the authentication causes it's impossible for us to create a new user. However, one way to solve this is to seed the database and create a user when the application boots up, and Vapor's Migration
protocol give us a perfect place to achieve it. Let's go back to our User
model and append the following lines at the bottom of the file.
Obviously, for production neither should we use password
as the password for our admin user, nor hardcode the password. One possible solution is to read the password from an environment variable. Last but not least, inside configure.swift
please append the following line under migrations.add(model: User.self, database: .sqlite)
.
This line adds our AdminUser
to the list of migrations so our application executes the migration at the next launch. Now, we are able to fulfill HTTP basic authentication with the username and password of our AdminUser
on Postman.
Token Authentication
At this stage, only authenticated users can create users, but all other endpoints are still unprotected. Besides, asking a user to give credentials with each request is impractical, and we don’t want to store a user’s password in our application. Instead, we should provide an endpoint for users to log in, and we can replace their credentials with a token after they log in.
Let’s start with our Token
class. As usual, open Terminal to create a new file and regenerate Xcode project as the followings.
Next, please write the following code into our new Token.swift
file.
Basically, we define our Token
model that contains the token string and the token owner's ID, and a helper method to generate a token for a login user. Then, Switch to configure.swift
and add the following line before services.register(migrations)
.
This line adds our Token
model to the list of migrations so our application executes the migration at the next launch. Open our UsersController
to write the new login method below deleteHandler
method.
Since we are going to protect this method with HTTP basic authentication, we can get the authenticated user from the request, and then create a token for the user. After this, inside boot(router:)
method, please add the following line under basicProtected.post(use: createHandler)
.
Now, we can run our application and create a token for our admin user with Postman.
Bearer token authentication is a mechanism for sending a token to authenticated requests, and it uses Authorization
header as well. Please go back to our Token
model and append the following code at the bottom of the file.
This extension tells Vapor what type the user is, which property the token is, and which one the user ID is. Then, switch to our User
model and add the following extension.
This extension tells Vapor what type a token is. The final step is to modify boot(router:)
method within our UsersController
. Let's replace the entire boot(router:)
method with the following code.
We use basicAuthMiddleware
and guardAuthMiddleware
to protect the login endpoint, and on the other hand, we use tokenAuthMiddleware
and guardAuthMiddleware
to protect our CRUD endpoints. Now, all of our endpoints are protected, and our application only accepts requests from authenticated users.
Conclusion
Here is the entire project.
Let’s recap this long journey of authentication. First of all, we add a new password
property of our User
model, and only store the hashed password into the database. Besides, we also prevent our application from returning the hashed passwords. Secondly, we install Vapor's authentication package and protect the endpoint of creating a user with HTTP basic authentication. Furthermore, we also seed the database with an admin user. Finally, we create our Token
model, implement the login endpoint, and protect the CRUD endpoints with bearer token authentication. Although it's a simple server, we still can see that Vapor actually provides very good authentication capabilities to add authentication to our endpoints.