Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

18. API Authentication, Part 1
Written by Tim Condon

The TILApp you’ve built so far has a ton of great features, but it also has one small problem: Anyone can create new users, categories or acronyms. There’s no authentication on the API or the website to ensure only known users can change what’s in the database. In this chapter, you’ll learn how to protect your API with authentication. You’ll learn how to implement both HTTP basic authentication and token authentication in your API. You’ll also learn best-practices for storing passwords and authenticating users.

Note: You must have PostgreSQL set up and configured in your project. If you still need to do this, follow the steps in Chapter 6, “Configuring a Database”.

Passwords

Authentication is the process of verifying who someone is. This is different from authorization, which is verifying that a user has permission to perform a particular action. You commonly authenticate users with a username and password combination and TILApp will be no different.

Open the Vapor application in Xcode and open User.swift. Add the following property to User below var username: String:

@Field(key: "password")
var password: String

This property stores the user’s password using the column name password. Next, to account for the new property, replace the initializer init(id:name:username) with the following:

init(
  id: UUID? = nil, 
  name: String, 
  username: String, 
  password: String
) {
  self.name = name
  self.username = username
  self.password = password
}

Password storage

Thanks to Codable, you don’t have to make any additional changes to create users with passwords. The existing UserController now automatically expects to find the password property in the incoming JSON. However, without any changes, you’ll be saving the user’s password in plain text.

You should never store passwords in plain text. You should always store passwords in a secure fashion. Bcrypt is an industry standard for hashing passwords and Vapor has it built in.

Bcrypt is a one-way hashing algorithm. This means that you can turn a password into a hash, but can’t convert a hash back into a password. Since Bcrypt is designed to be slow, if someone steals a password hash, it takes a long time to brute-force the password. Bcrypt hashes a salt with the password. A salt is a unique, random value to help defend against common attacks. Bcrypt also provides a mechanism to verify a password using the password and a hash.

Open UsersController.swift, find createHandler(_:user:) and add the following after let user = try req.content.decode(User.self):

user.password = try Bcrypt.hash(user.password)

This hashes the user’s password before saving it in the database.

Making usernames unique

In the coming sections of this chapter, you’ll be using the username and password to uniquely identify users. At the moment, there’s nothing to prevent multiple users from having the same username.

Open CreateUser.swift. Before .create() add:

.field("password", .string, .required)
.unique(on: "username")

This updates the migration to add a field for the password and a unique index to username of User. After the application runs the updated migration, any attempts to create duplicate usernames result in an error.

Fixing the tests

You changed the initializer for User so you need to update the tests so Xcode can compile your app. Open UserTests.swift and in testUserCanBeSavedWithAPI() replace let user = User... with the following:

let user = User(
  name: usersName, 
  username: usersUsername, 
  password: "password")

Next, open Models+Testable.swift and update create(name:username:on:) in the extension for User. Again, add a value for the password parameter:

let user = User(
  name: name, 
  username: username, 
  password: "password")

Returning users from the API

Since the model has changed, you need to reset the database. Fluent has already run the User migration, but the table has a new column now. To add the new column to the table, you must delete the database so Fluent will run the migration again. In Terminal, enter:

# 1
docker stop postgres
# 2
docker rm postgres
# 3
docker run --name postgres -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres

Here’s what this does:

  1. Stop the running Docker container postgres. This is the container currently running the database.
  2. Remove the Docker container postgres to delete any existing data.
  3. Start a new Docker container running PostgreSQL. For more information, see Chapter 6, “Configuring a Database”.

Now, build and run and Fluent will create a clean database with your new additions.

Launch RESTed, create a new request and configure it as follows:

Add three parameters with names and values:

  • name: your name
  • username: a username of your choice
  • password: a password of your choice

Click Send Request. Your application creates the requested user, but the response returns the password hash:

This isn’t good! You should protect password hashes and never return them in responses. In fact, any user returned by the API includes the password hash, including listing all the users! This happens because you’re returning User in all your routes. You should instead return a “public view” of User.

In Xcode, open User.swift and add the following below the User initializer:

final class Public: Content {
  var id: UUID?
  var name: String
  var username: String

  init(id: UUID?, name: String, username: String) {
    self.id = id
    self.name = name
    self.username = username
  }
}

This creates an inner class to represent a public view of User to return in responses. Next, add the following at the bottom of User.swift:

extension User {
  // 1
  func convertToPublic() -> User.Public {
    // 2
    return User.Public(id: id, name: name, username: username)
  }
}

Here’s what the new method does:

  1. Define a method on User that returns User.Public.
  2. Create a public version of the current object.

Finally, add the following below the new extension:

// 1
extension EventLoopFuture where Value: User {
  // 2
  func convertToPublic() -> EventLoopFuture<User.Public> {
    // 3
    return self.map { user in
      // 4
      return user.convertToPublic()
    }
  }
}

// 5
extension Collection where Element: User {
  // 6
  func convertToPublic() -> [User.Public] {
    // 7
    return self.map { $0.convertToPublic() }
  }
}

// 8
extension EventLoopFuture where Value == Array<User> {
  // 9
  func convertToPublic() -> EventLoopFuture<[User.Public]> {
    // 10
    return self.map { $0.convertToPublic() }
  }
}

Here’s what this does:

  1. Define an extension for EventLoopFuture<User>.
  2. Define a new method that returns a EventLoopFuture<User.Public>.
  3. Unwrap the user contained in self.
  4. Convert the User object to User.Public.
  5. Define an extension for [User].
  6. Define a new method that returns [User.Public].
  7. Convert all the User objects in the array to User.Public.
  8. Define an extension for EventLoopFuture<[User]>.
  9. Define a new method that returns EventLoopFuture<[User.Public]>.
  10. Unwrap the array contained in the future and use the previous extension to convert all the Users to User.Public.

These extensions allow you to call convertToPublic() on EventLoopFuture<User>, [User] and EventLoopFuture<[User]>. This helps tidy up your code and reduce nesting. These new methods allow you to change your route handlers to return public users.

First, open UsersController.swift and change the return type of createHandler(_:user:):

func createHandler(_ req: Request)
  -> EventLoopFuture<User.Public> {

Next, change the result of map to return a public user instead:

return user.save(on: req.db).map { user.convertToPublic() }

This uses the new method to convert a User to User.Public. Build and run, then create a new user in RESTed. You’ll notice the user’s password hash is no longer returned:

Now, you must update the rest of the routes that return User.

First, in UsersController.swift change the signature of getAllHandler(_:) to the following:

func getAllHandler(_ req: Request)
  -> EventLoopFuture<[User.Public]> {

Next, change the body of getAllHandler(_:) to the following:

User.query(on: req.db).all().convertToPublic()

This uses the extension for EventLoopFuture<[User]> to convert the users returned from the database to User.Public. Next, change the signature of getHandler(_:) to return a public user:

func getHandler(_ req: Request) 
  -> EventLoopFuture<User.Public> {

Next, change the body to return a public user:

User.find(req.parameters.get("userID"), on: req.db)
  .unwrap(or: Abort(.notFound))
  .convertToPublic()

Finally, open AcronymsController.swift and replace getUserHandler(_:) so it returns a public user:

// 1
func getUserHandler(_ req: Request) 
  -> EventLoopFuture<User.Public> {
  Acronym.find(req.parameters.get("acronymID"), on: req.db)
  .unwrap(or: Abort(.notFound))
  .flatMap { acronym in
    // 2
    acronym.$user.get(on: req.db).convertToPublic()
  }
}

Here’s what changed:

  1. Change the return type of the method to Future<User.Public>.
  2. Call convertToPublic() on the acronym’s user to return a public user.

Now, no calls to your API to retrieve a user will return a password hash.

Basic authentication

HTTP basic authentication is a standardized method of sending credentials via HTTP and is defined by RFC 7617 (https://tools.ietf.org/html/rfc7617). You typically include the credentials in an HTTP request’s Authorization header.

To generate the token for this header, you combine the username and password, then Base64-encode the result.

For example, for the username timc and password password the combined credential string is:

timc:password

You then Base64-encode this which gives you:

dGltYzpwYXNzd29yZA==

The full header becomes:

Authorization: Basic dGltYzpwYXNzd29yZA==

Authentication is built into Vapor and contains helpers to use HTTP Basic authentication. Open User.swift and, at the bottom of the file, add the following:

// 1
extension User: ModelAuthenticatable {
  // 2
  static let usernameKey = \User.$username
  // 3
  static let passwordHashKey = \User.$password

  // 4
  func verify(password: String) throws -> Bool {
    try Bcrypt.verify(password, created: self.password)
  }
}

Here’s what this does:

  1. Conform User to ModelAuthenticatable. This is a protocol that allows Fluent Models to use HTTP Basic Authentication.
  2. Tell Vapor which key path of User is the username.
  3. Tell Vapor which key path of User is the password hash.
  4. Implement verify(password:) as required by ModelAuthenticatable. Since you hash the User’s password using Bcrypt, verify the hash with Bcrypt here.

Open AcronymsController.swift and add the following at the bottom of boot(routes:):

// 1
let basicAuthMiddleware = User.authenticator()
// 2
let guardAuthMiddleware = User.guardMiddleware()
// 3
let protected = acronymsRoutes.grouped(
  basicAuthMiddleware,
  guardAuthMiddleware)
// 4
protected.post(use: createHandler)

Here’s what this does:

  1. Create an instance of ModelAuthenticator middleware, which uses HTTP Basic Authentication. Since User conforms to ModelAuthenticatable, this is available as a static method on the model.
  2. Create an instance of GuardAuthenticationMiddleware which ensures that requests contain authenticated users.
  3. Create a middleware group which uses basicAuthMiddleware and guardAuthMiddleware.
  4. Connect the “create acronym” path to createHandler(_:acronym:) through this middleware group.

Middleware allows you to intercept requests and responses in your application. In this example, basicAuthMiddleware intercepts the request and authenticates the user supplied. You can chain middleware together. In the above example, basicAuthMiddleware authenticates the user. Then guardAuthMiddleware ensures the request contains an authenticated user. If there’s no authenticated user, guardAuthMiddleware throws an error. You can learn more about middleware in Chapter 29, “Middleware”.

This ensures only requests authenticated using HTTP basic authentication can create acronyms.

Next, delete the following to remove the unauthenticated route:

acronymsRoutes.post(use: createHandler)

Build and run, then launch RESTed. Create a new request and configure it as follows:

Add three parameters with names and values:

  • short: OMG
  • long: Oh My God
  • userID: The ID of the user created earlier

Click Send Request and you’ll receive a 401 Unauthorized error response. You should see the following:

In RESTed, click Authorization and enter the username and password for the user created earlier. Check Present Before Authentication Challenge and click OK:

This sets the basic Authorization header as described above. Click Send Request again. This time the request succeeds:

Token authentication

Getting a token

At this stage, only authenticated users can create acronyms. However, all other “destructive” routes are still unprotected. Asking a user to enter credentials with each request is impractical. You also don’t want to store a user’s password anywhere in your application since you’d have to store it in plain text. Instead, you’ll allow users to log in to your API. When they log in, you exchange their credentials for a token the client can save.

Create a new file, Token.swift in Sources/App/Models. Open the new file and add the following:

import Vapor
import Fluent

final class Token: Model, Content {
  static let schema = "tokens"

  @ID
  var id: UUID?

  @Field(key: "value")
  var value: String

  @Parent(key: "userID")
  var user: User

  init() {}

  init(id: UUID? = nil, value: String, userID: User.IDValue) {
    self.id = id
    self.value = value
    self.$user.id = userID
  }
}

This defines a model for Token that contains the following properties:

  • id: the ID of the model.
  • value: the token string provided to clients.
  • user: a @Parent field to the token owner’s user.

Create a migration file, CreateToken.swift in Sources/App/Migrations, for the new model and insert the migration below:

import Fluent

struct CreateToken: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    database.schema("tokens")
      .id()
      .field("value", .string, .required)
      .field(
        "userID", 
        .uuid, 
        .required,
        .references("users", "id", onDelete: .cascade))
      .create()
  }

  func revert(on database: Database) -> EventLoopFuture<Void> {
    database.schema("tokens").delete()
  }
}

Like other migrations before, this creates the table for Token. It also creates a reference to User for the userID field. The reference is marked with a cascade deletion so that any tokens are automatically deleted when you delete a user. In configure.swift, add the following after app.migrations.add(CreateAcronymCategoryPivot()):

app.migrations.add(CreateToken())

This adds CreateToken to the list of migrations so Vapor creates the table when the application next starts. When a user logs in, the application must create a token for that user.

Open Token.swift and add the following at the bottom of the file:

extension Token {
  // 1
  static func generate(for user: User) throws -> Token {
    // 2
    let random = [UInt8].random(count: 16).base64
    // 3
    return try Token(value: random, userID: user.requireID())
  }
}

Here’s what this extension does:

  1. Define a static method to generate a token for a user.
  2. Generate 16 random bytes to act as the token and Base64 encode it.
  3. Create a Token using the Base64-encoded representation of the random bytes and the user’s ID.

Open UsersController.swift and add the following under getAcronymsHandler(_:):

// 1
func loginHandler(_ req: Request) throws 
  -> EventLoopFuture<Token> {
  // 2
  let user = try req.auth.require(User.self)
  // 3
  let token = try Token.generate(for: user)
  // 4
  return token.save(on: req.db).map { token }
}

Here’s what this does:

  1. Define a route handler for logging a user in.
  2. Get the authenticated user from the request. You’ll protect this route with the HTTP basic authentication middleware. This saves the user’s identity in the request’s authentication cache, allowing you to retrieve the user object later. req.auth.require(_:) throws an authentication error if there’s no authenticated user.
  3. Create a token for the user.
  4. Save and return the token.

At the bottom of boot(routes:) add the following:

// 1
let basicAuthMiddleware = User.authenticator()
let basicAuthGroup = usersRoute.grouped(basicAuthMiddleware)
// 2
basicAuthGroup.post("login", use: loginHandler)

Here’s what this does:

  1. Create a protected route group using HTTP basic authentication, as you did for creating an acronym. This doesn’t use GuardAuthenticationMiddleware since req.auth.require(_:) throws the correct error if a user isn’t authenticated.
  2. Connect /api/users/login to loginHandler(_:) through the protected group.

Build and run, then head back to RESTed.

Ensure you’ve configured the HTTP basic authentication and set the URL to http://localhost:8080/api/users/login.

Click Send Request and you’ll receive a token back:

Using a token

Open Token.swift and add the following at the end of the file:

// 1
extension Token: ModelTokenAuthenticatable {
  // 2
  static let valueKey = \Token.$value
  // 3
  static let userKey = \Token.$user
  // 4
  typealias User = App.User
  // 5
  var isValid: Bool {
    true
  }
}

Here’s what this does:

  1. Conform Token to Vapor’s ModelTokenAuthenticatable protocol. This allows you to use the token with HTTP Bearer authentication.
  2. Tell Vapor the key path to the value key, in this case, Token’s value projected value.
  3. Tell Vapor the key path to the user key, in this case, Token’s user projected value.
  4. Tell Vapor what type the user is.
  5. Determine if the token is valid. Return true for now, but you might add an expiry date or a revoked property to check in the future.

Bearer authentication is a mechanism for sending a token to authenticate requests. It uses the Authorization header, like HTTP basic authentication, but the header looks like Authorization: Bearer <TOKEN STRING>.

Currently when users create acronyms, they must send their ID in the request. However, because you’re requiring authentication, you now know which user sent each request. In AcronymsController.swift, remove let userID: UUID from CreateAcronymData. Next, in createHandler(_:), replace:

let acronym = Acronym(
  short: data.short,
  long: data.long,
  userID: data.userID)

with the following:

// 1
let user = try req.auth.require(User.self)
// 2
let acronym = try Acronym(
  short: data.short, 
  long: data.long,
  userID: user.requireID())

The changes made were:

  1. Get the authenticated user from the request.
  2. Create a new Acronym using the data from the request and the authenticated user.

Next, replace updateHandler(_:) with the following:

func updateHandler(_ req: Request) throws 
  -> EventLoopFuture<Acronym> {
  let updateData = 
    try req.content.decode(CreateAcronymData.self)
  // 1
  let user = try req.auth.require(User.self)
  // 2
  let userID = try user.requireID()
  return Acronym
    .find(req.parameters.get("acronymID"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { acronym in
      acronym.short = updateData.short
      acronym.long = updateData.long
      // 3
      acronym.$user.id = userID
      return acronym.save(on: req.db).map {
        acronym
      }
  }
}

The changes made were:

  1. Get the authenticated user from the request.
  2. Get the user ID from the user. It’s useful to do this here as you can’t throw inside flatMap(_:).
  3. Set the acronym’s user’s ID to the user ID from the step above.

Finally, update the tests so the project compiles. Open AcronymTests.swift. In testAcronymCanBeSavedWithAPI() replace let createAcronymData = ... with the following:

let createAcronymData = 
  CreateAcronymData(short: acronymShort, long: acronymLong)

This removes the userID parameter as it’s no longer required. While you’re there, remove the line let user = try User.create... since it’s no longer needed. Finally, in testUpdatingAnAcronym() replace let updatedAcronymData = ... with the following to remove the extra userID parameter:

let updatedAcronymData = 
  CreateAcronymData(short: acronymShort, long: newLong)

Return to AcronymsController.swift. In boot(routes:), remove the code you used earlier to protect the “create an acronym” route and replace it with the following:

// 1
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
// 2
let tokenAuthGroup = acronymsRoutes.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
// 3
tokenAuthGroup.post(use: createHandler)

Here’s what the new code does:

  1. Create a ModelTokenAuthenticator middleware for Token. This extracts the bearer token out of the request and converts it into a logged in user.
  2. Create a route group using tokenAuthMiddleware and guardAuthMiddleware to protect the route for creating an acronym with token authentication.
  3. Connect the “create acronym” path to createHandler(_:data:) through this middleware group using the new AcronymCreateData.

Build and run, then head back to RESTed. Copy the token value string returned from the user login. Configure a request like so:

Add two parameters with names and values:

  • short: IKR
  • long: I Know Right

Create a new header field for Authorization with the value Bearer <TOKEN STRING>, using the token string you copied earlier. Remove the HTTP basic authentication credentials you used for logging in.

To do this, click Authorization, remove the username and password, and uncheck Present Before Authentication Challenge.

Click Send Request and you’ll see the created acronym returned:

Open AcronymsController.swift, find boot(routes:), and delete the following lines:

acronymsRoutes.put(":acronymID", use: updateHandler)
acronymsRoutes.delete(":acronymID", use: deleteHandler)
acronymsRoutes.post(":acronymID", "categories", ":categoryID", 
                    use: addCategoriesHandler)
acronymsRoutes.delete(":acronymID", "categories", ":categoryID", 
                      use: removeCategoriesHandler)

This is all of the original routes that are not get() routes. At the bottom of boot(routes:), add their replacements:

tokenAuthGroup.delete(":acronymID", use: deleteHandler)
tokenAuthGroup.put(":acronymID", use: updateHandler)
tokenAuthGroup.post(
  ":acronymID", 
  "categories", 
  ":categoryID",
  use: addCategoriesHandler)
tokenAuthGroup.delete(
  ":acronymID", 
  "categories", 
  ":categoryID",
  use: removeCategoriesHandler)

This ensures that only authenticated users can create, edit and delete acronyms, and add categories to acronyms. Unauthenticated users can still view details about acronyms.

Now, open CategoriesController.swift and, in boot(routes:), delete categoriesRoute.post(use: createHandler).

Replace it with the following at the end of the method:

let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
let tokenAuthGroup = categoriesRoute.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
tokenAuthGroup.post(use: createHandler)

This uses the token middleware to protect category creation, just like creating an acronym, ensuring only authenticated users can create categories. Finally, open UsersController.swift and delete usersRoute.post(use: createHandler). At the bottom of boot(routes:), add the following:

let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
let tokenAuthGroup = usersRoute.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
tokenAuthGroup.post(use: createHandler)

Again, using tokenAuthMiddleware and guardAuthMiddleware ensures only authenticated users can create other users. This prevents anyone from creating a user to send requests to the routes you’ve just protected!

Now all API routes that can perform “destructive” actions — that is create, edit or delete resources — are protected. For those actions, the application only accept requests from authenticated users.

Database seeding

At this point the API is secure, but now there’s another problem. When you deploy your application, or next revert the database, you won’t have any users in the database.

But, you can’t create a new user since that route requires authentication! One way to solve this is to seed the database and create a user when the application first boots up. In Vapor, you do this with a migration.

In Sources/App/Migrations create a new file, CreateAdminUser.swift. Open the new file and add the following:

import Fluent
import Vapor

// 1
struct CreateAdminUser: Migration {
  // 2
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    // 3
    let passwordHash: String
    do {
      passwordHash = try Bcrypt.hash("password")
    } catch {
      return database.eventLoop.future(error: error)
    }
    // 4
    let user = User(
      name: "Admin", 
      username: "admin",
      password: passwordHash)
    // 5
    return user.save(on: database)
  }

  // 6
  func revert(on database: Database) -> EventLoopFuture<Void> {
    // 7
    User.query(on: database)
      .filter(\.$username == "admin")
      .delete()
  }
}

Here’s what this does:

  1. Define a new type that conforms to Migration.
  2. Implement the required prepare(on:).
  3. Create a password hash from the password. Catch any errors thrown and return a failed future.
  4. Create a new user with the name Admin, username admin and the hashed password.
  5. Save the user and return.
  6. Implement the required revert(on:).
  7. Query User and delete any rows where the username matches admin. As usernames must be unique, this only deletes the one admin row.

Note: Obviously, in a production system, you shouldn’t use password as the password for your admin user! You also don’t want to hard code the password in case it ends up in source control. You can either read an environment variable or generate a random password and print it out.

Open configure.swift and add the following after app.migrations.add(CreateToken()):

app.migrations.add(CreateAdminUser())

This adds CreateAdminUser to the list of migrations so the app executes the migration at the next app launch.

Build and run. Head to RESTed and try out all of your newly protected routes. You can even log in with the new admin user.

Where to go from here?

In this chapter, you learned about HTTP Basic and Bearer authentication. You saw how authentication middleware can simplify your code and do much of the heavy lifting for you. You saw how to modify your existing model to work with Vapor’s authentication capabilities. You glued it all together to add authentication to your API.

But, there’s much more to be done. Turn the page and get busy updating your test suite and your iOS app to work with the new authentication capabilities.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.