Home iOS & Swift Books Server-Side Swift with Vapor

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.

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

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)
  }
}
// 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() }

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. You typically include the credentials in an HTTP request’s Authorization header.

timc:password
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)
acronymsRoutes.post(use: createHandler)

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())
  }
}
// 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
  }
}
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
      }
  }
}
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)

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)

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.