Home iOS & Swift Books Server-Side Swift with Vapor

18
API Authentication, Part 1 Written by Tim Condon

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

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:

var password: String

This property stores the user’s password. Next, to account for the new property, replace the initializer with the following:

init(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.

import Crypto
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.

extension User: Migration {}
extension User: Migration {
  static func prepare(on connection: PostgreSQLConnection)
    -> Future<Void> {
    // 1
    return Database.create(self, on: connection) { builder in
      // 2
      try addProperties(to: builder)
      // 3
      builder.unique(on: \.username)
    }
  }
}

Returning users from the API

Since the model has changed you need to revert your database so Vapor can add the new column to the table. Option-Click the Run button in Xcode — or press Option-Command-R — to open the scheme editor. On the Arguments tab, click + in the Arguments Passed On Launch section, and enter:

revert --all --yes

final class Public: Codable {
  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.Public: Content {}
extension User {
  // 1
  func convertToPublic() -> User.Public {
    // 2
    return User.Public(id: id, name: name, username: username)
  }
}
// 1
extension Future where T: User {
  // 2
  func convertToPublic() -> Future<User.Public> {
    // 3
    return self.map(to: User.Public.self) { user in
      // 4
      return user.convertToPublic()
    }
  }
}
func createHandler(_ req: Request, user: User) throws
  -> Future<User.Public> {
return user.save(on: req).convertToPublic()

func getAllHandler(_ req: Request) throws
  -> Future<[User.Public]> {
return User.query(on: req).decode(data: User.Public.self).all()
func getHandler(_ req: Request) throws -> Future<User.Public> {
return try req.parameters.next(User.self).convertToPublic()
// 1
func getUserHandler(_ req: Request) throws
  -> Future<User.Public> {
  // 2
  return try req.parameters.next(Acronym.self)
    .flatMap(to: User.Public.self) { acronym in
      // 3
      acronym.user.get(on: req).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==
.package(
  url: "https://github.com/vapor/leaf.git",
  from: "3.0.0"),
.package(
  url: "https://github.com/vapor/auth.git",
  from: "2.0.0")
dependencies: ["FluentPostgreSQL",
               "Vapor",
               "Leaf",
               "Authentication"]
vapor xcode -y
import Authentication
// 1
extension User: BasicAuthenticatable {
  // 2
  static let usernameKey: UsernameKey = \User.username
  // 3
  static let passwordKey: PasswordKey = \User.password
}
import Authentication
// 1
let basicAuthMiddleware =
  User.basicAuthMiddleware(using: BCryptDigest())
// 2
let guardAuthMiddleware = User.guardAuthMiddleware()
// 3
let protected = acronymsRoutes.grouped(
  basicAuthMiddleware,
  guardAuthMiddleware)
// 4
protected.post(Acronym.self, use: createHandler)
acronymsRoutes.post(Acronym.self, use: createHandler)
import Authentication
try services.register(AuthenticationProvider())

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.

# 1
touch Sources/App/Models/Token.swift
# 2
vapor xcode -y
import Foundation
import Vapor
import FluentPostgreSQL
import Authentication

final class Token: Codable {
  var id: UUID?
  var token: String
  var userID: User.ID

  init(token: String, userID: User.ID) {
    self.token = token
    self.userID = userID
  }
}

extension Token: PostgreSQLUUIDModel {}

extension Token: Migration {
  static func prepare(on connection: PostgreSQLConnection)
    -> Future<Void> {
      return Database.create(self, on: connection) { builder in
        try addProperties(to: builder)
        builder.reference(from: \.userID, to: \User.id)
      }
  }
}

extension Token: Content {}
migrations.add(model: Token.self, database: .psql)
extension Token {
  // 1
  static func generate(for user: User) throws -> Token {
    // 2
    let random = try CryptoRandom().generateData(count: 16)
    // 3
    return try Token(
      token: random.base64EncodedString(),
      userID: user.requireID())
  }
}
// 1
func loginHandler(_ req: Request) throws -> Future<Token> {
  // 2
  let user = try req.requireAuthenticated(User.self)
  // 3
  let token = try Token.generate(for: user)
  // 4
  return token.save(on: req)
}
// 1
let basicAuthMiddleware =
  User.basicAuthMiddleware(using: BCryptDigest())
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: Authentication.Token {
  // 2
  static let userIDKey: UserIDKey = \Token.userID
  // 3
  typealias UserType = User
}

// 4
extension Token: BearerAuthenticatable {
  // 5
  static let tokenKey: TokenKey = \Token.token
}
// 1
extension User: TokenAuthenticatable {
  // 2
  typealias TokenType = Token
}
struct AcronymCreateData: Content {
  let short: String
  let long: String
}
// 1
func createHandler(
  _ req: Request,
  data: AcronymCreateData
) throws -> Future<Acronym> {
  // 2
  let user = try req.requireAuthenticated(User.self)
  // 3
  let acronym = try Acronym(
    short: data.short, 
    long: data.long,
    userID: user.requireID())
  // 4
  return acronym.save(on: req)
}
// 1
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardAuthMiddleware = User.guardAuthMiddleware()
// 2
let tokenAuthGroup = acronymsRoutes.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
// 3
tokenAuthGroup.post(AcronymCreateData.self, use: createHandler)

acronymsRoutes.put(Acronym.parameter, use: updateHandler)
acronymsRoutes.delete(Acronym.parameter, use: deleteHandler)
acronymsRoutes.post(
  Acronym.parameter,
  "categories",
  Category.parameter,
  use: addCategoriesHandler)
acronymsRoutes.delete(
  Acronym.parameter,
  "categories",
  Category.parameter,
  use: removeCategoriesHandler)
tokenAuthGroup.delete(Acronym.parameter, use: deleteHandler)
tokenAuthGroup.put(Acronym.parameter, use: updateHandler)
tokenAuthGroup.post(
  Acronym.parameter,
  "categories",
  Category.parameter,
  use: addCategoriesHandler)
tokenAuthGroup.delete(
  Acronym.parameter,
  "categories",
  Category.parameter,
  use: removeCategoriesHandler)
func updateHandler(_ req: Request) throws -> Future<Acronym> {
  // 1
  return try flatMap(
    to: Acronym.self,
    req.parameters.next(Acronym.self),
    req.content.decode(AcronymCreateData.self)
  ) { acronym, updateData in
      acronym.short = updateData.short
      acronym.long = updateData.long
      // 2
      let user = try req.requireAuthenticated(User.self)
      acronym.userID = try user.requireID()
      return acronym.save(on: req)
  }
}
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardAuthMiddleware = User.guardAuthMiddleware()
let tokenAuthGroup = categoriesRoute.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
tokenAuthGroup.post(Category.self, use: createHandler)
let tokenAuthMiddleware = User.tokenAuthMiddleware()
let guardAuthMiddleware = User.guardAuthMiddleware()
let tokenAuthGroup = usersRoute.grouped(
  tokenAuthMiddleware,
  guardAuthMiddleware)
tokenAuthGroup.post(User.self, 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.

// 1
struct AdminUser: Migration {
  // 2
  typealias Database = PostgreSQLDatabase

  // 3
  static func prepare(on connection: PostgreSQLConnection)
    -> Future<Void> {
    // 4
    let password = try? BCrypt.hash("password")
    guard let hashedPassword = password else {
      fatalError("Failed to create admin user")
    }
    // 5
    let user = User(
      name: "Admin",
      username: "admin",
      password: hashedPassword)
    // 6
    return user.save(on: connection).transform(to: ())
  }

  // 7
  static func revert(on connection: PostgreSQLConnection)
    -> Future<Void> {
    return .done(on: connection)
  }
}
migrations.add(migration: AdminUser.self, database: .psql)

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:

© 2020 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.