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

Heads up... You’re accessing parts of this content for free, with some sections shown as qrgexczim text.

Heads up... You’re accessing parts of this content for free, with some sections shown as tfxyxmpym text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as jmqurlbux text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gflenbrig text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

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

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.

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

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")
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

Heads up... You’re accessing parts of this content for free, with some sections shown as smzymqqes text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qdpyxjqof text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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
  }
}
extension User {
  // 1
  func convertToPublic() -> User.Public {
    // 2
    return User.Public(id: id, name: name, username: username)
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as gtvaxvwuf text.

Heads up... You’re accessing parts of this content for free, with some sections shown as mcfagppid text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
// 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() }
  }
}
func createHandler(_ req: Request)
  -> EventLoopFuture<User.Public> {
return user.save(on: req.db).map { user.convertToPublic() }

Heads up... You’re accessing parts of this content for free, with some sections shown as vctybxhil text.

Heads up... You’re accessing parts of this content for free, with some sections shown as chjuskbyp text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
func getAllHandler(_ req: Request)
  -> EventLoopFuture<[User.Public]> {
User.query(on: req.db).all().convertToPublic()
func getHandler(_ req: Request) 
  -> EventLoopFuture<User.Public> {
User.find(req.parameters.get("userID"), on: req.db)
  .unwrap(or: Abort(.notFound))
  .convertToPublic()
// 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()
  }
}

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.

timc:password

Heads up... You’re accessing parts of this content for free, with some sections shown as jlniqzdof text.

Heads up... You’re accessing parts of this content for free, with some sections shown as nrgalnmyd text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
dGltYzpwYXNzd29yZA==
Authorization: Basic dGltYzpwYXNzd29yZA==
// 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)
  }
}
// 1
let basicAuthMiddleware = User.authenticator()
// 2
let guardAuthMiddleware = User.guardMiddleware()
// 3
let protected = acronymsRoutes.grouped(
  basicAuthMiddleware,
  guardAuthMiddleware)
// 4
protected.post(use: createHandler)

Heads up... You’re accessing parts of this content for free, with some sections shown as fmjepcruw text.

Heads up... You’re accessing parts of this content for free, with some sections shown as kxhosjmyt text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
acronymsRoutes.post(use: createHandler)

Heads up... You’re accessing parts of this content for free, with some sections shown as klkonqnin text.

Heads up... You’re accessing parts of this content for free, with some sections shown as nhfortzup text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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
  }
}
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()
  }
}
app.migrations.add(CreateToken())
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())
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as xnqyrmvyz text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lkfizpryr text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
// 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 }
}
// 1
let basicAuthMiddleware = User.authenticator()
let basicAuthGroup = usersRoute.grouped(basicAuthMiddleware)
// 2
basicAuthGroup.post("login", use: loginHandler)

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
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as ljsalwzoz text.

Heads up... You’re accessing parts of this content for free, with some sections shown as jsdulmpej text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
let acronym = Acronym(
  short: data.short,
  long: data.long,
  userID: data.userID)
// 1
let user = try req.auth.require(User.self)
// 2
let acronym = try Acronym(
  short: data.short, 
  long: data.long,
  userID: user.requireID())
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
      }
  }
}

Heads up... You’re accessing parts of this content for free, with some sections shown as szziddwaf text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cqtorcceq text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
let createAcronymData = 
  CreateAcronymData(short: acronymShort, long: acronymLong)
let updatedAcronymData = 
  CreateAcronymData(short: acronymShort, long: newLong)
// 1
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
// 2
let tokenAuthGroup = acronymsRoutes.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
// 3
tokenAuthGroup.post(use: createHandler)

Heads up... You’re accessing parts of this content for free, with some sections shown as rjhajvbyp text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gxfynlwaj text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

acronymsRoutes.put(":acronymID", use: updateHandler)
acronymsRoutes.delete(":acronymID", use: deleteHandler)
acronymsRoutes.post(":acronymID", "categories", ":categoryID", 
                    use: addCategoriesHandler)
acronymsRoutes.delete(":acronymID", "categories", ":categoryID", 
                      use: removeCategoriesHandler)
tokenAuthGroup.delete(":acronymID", use: deleteHandler)
tokenAuthGroup.put(":acronymID", use: updateHandler)
tokenAuthGroup.post(
  ":acronymID", 
  "categories", 
  ":categoryID",
  use: addCategoriesHandler)
tokenAuthGroup.delete(
  ":acronymID", 
  "categories", 
  ":categoryID",
  use: removeCategoriesHandler)
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
let tokenAuthGroup = categoriesRoute.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
tokenAuthGroup.post(use: createHandler)
let tokenAuthMiddleware = Token.authenticator()
let guardAuthMiddleware = User.guardMiddleware()
let tokenAuthGroup = usersRoute.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
tokenAuthGroup.post(use: createHandler)

Heads up... You’re accessing parts of this content for free, with some sections shown as vktibkdep text.

Heads up... You’re accessing parts of this content for free, with some sections shown as hmwerrlef text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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()
  }
}
app.migrations.add(CreateAdminUser())

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.

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.
© 2025 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as rnmuvbwyc text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now