UIKit Apprentice, Second Edition – Now Updated!

Learn iOS and Swift from scratch. Build four powerful apps—with support for iPad and Dark Mode. Publish apps to the App Store.

Home Server-Side Swift Tutorials

Two-Factor Authentication With Vapor

Learn how to increase the account security of your using two-factor authentication with Vapor.

5/5 2 Ratings

Version

  • Swift 5, macOS 11, Xcode 12

The internet is a great place, but it can also be a scary place. With regular reports of hacked companies and leaked credentials, account security is more important than ever. A great way to add an extra layer of security to a user’s account is to provide the option of two-factor authentication with Vapor, which uses cryptographic algorithms to generate one-time use passwords. As the name suggests, two-factor authentication (2FA) adds a second layer of authentication for logging into an account.

In this tutorial you’ll learn how to:

  • Create one-time passwords and understand what they are.
  • Add two-factor authentication to your app.
  • Extract two-factor authentication into reusable middleware.

You’ll do this by taking an existing app and adding 2FA to it.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

The sample app, DiningIn, lets users host dinners and invite friends to join. Along with the sample app, there are Paw and Postman files you can use to test the existing routes like logging in and registering.

Luckily, the app is already fully functional, so all you need to worry about is adding 2FA. :]

The tutorial expands on Vapor 4 Authentication: Getting Started, so be sure to follow along with that tutorial before diving into this one.

But before you get into the implementation details, you’ll first learn a bit about 2FA.

Understanding Two-Factor Authentication

There are many ways to add a second layer of authentication to the login process. One of the more popular ways of adding this layer is via one-time passwords (OTPs). OTPs are codes that will only work once to complete the login cycle. They’re delivered through a text message, as an email, or via the concept you’ll be diving into today, cryptographic algorithms.

Note: If you’d like to learn more about SMS authentication, check out SMS User Authentication With Vapor and AWS

You can generate cryptographic OTPs using hash-based message authentication codes (HMACs). To generate a HMAC, you need a key, a message and a hash function. Like with any hash function, HMAC will always give the same output for a given input.

Next, the server will generate a key and share that with the client. This key is stored in an app like Authy or Google Authenticator, which will use HMAC to generate a 6-, 7-, or 8-digit code. As mentioned earlier, HMAC also needs a message. There are two types of cryptographic OTPs: hash based (HOTP) and time based (TOTP).

Hash Based One Time Passwords

With hash-based OTP, the message is an integer that gets incremented with every login attempt. So the first code has the message 0x01, the second code has 0x02 and so on. The downside to this approach is both the client and server need to keep track of this counter, and if they lose sync, the OTP setup fails.

Time Based One Time Passwords

The alternative, TOTP, uses timestamps to generate the message. In most cases, TOTP codes refresh every 30 seconds, so the message is the amount of times (rounded down) 30 fits in the current seconds since 1970. (January 1st, 1970 is used a reference date for computers to synchronize time-based procedures). So if the time were one minute and five seconds past midnight on January 1st 1970, the message would be 0x03. The great benefit of TOTP is the only part that needs to be in sync between the server and the client is the key.

OTP and TOTP for two-factor authentication

The hash function is the final part to calculate the HMAC. The current TOTP and HOTP specs support three variants of Secure Hash Algorithm (SHA) functions: SHA-1, SHA-256 and SHA-512. In most cases, SHA-1 isn’t considered cryptographically secure, but in combination with HMAC, it’s fine to use. It’s also the only form of hash function that Google Authenticator accepts, so for 2FA, there’s not much of a choice.

This tutorial won’t cover specific hash functions and how they work, but if you’re interested in reading more, refer to the end for some reading suggestions.

Setting up 2FA

Now that you know about 2FA, it’s time to get building. Open the starter project and create a new file in Models called 2FAToken.swift with the following:

import Fluent
import Vapor

final class TwoFactorToken: Model {
  struct Public: Content {
    let backupCodes: [String]
    let key: String
    let label: String
    let issuer: String
    let url: String
  }
  
  static let schema = "twofactor_tokens"

  @ID(key: "id")
  var id: UUID?

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

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

  @Field(key: "backup_tokens")
  var backupTokens: [String]

  init() {}

  init(_ userId: User.IDValue, _ key: String, _ backups: [String]) {
    self.$user.id = userId
    self.key = key
    self.backupTokens = backups
  }
}

Here, you create a model to store the key for HMAC, along with a list of backup tokens for specific user.

OTP Tokens

Next, you’ll add all functionality to generate, retrieve and validate OTP tokens.

Generating OTP Tokens

To start off, create an extension to TwoFactorToken:

extension TwoFactorToken {
  var totpToken: TOTP {
    let key = SymmetricKey(data: Data.init(base32Encoded: self.key)!)
    return TOTP(key: key, digest: .sha1, digits: .six, interval: 30)
  }
}

Vapor’s security module exposes the TOTP type and provides you with all the APIs you need to generate tokens. This is also where you configure your hash function, the amount of digits and the interval at which the code should refresh.

Now add a new function to the extension to validate a user-submitted OTP:

func validate(_ input: String, allowBackupCode: Bool = true) -> Bool {
  self.totpToken.generate(time: Date(), range: 1).contains(input) || 
    (allowBackupCode && self.backupTokens.contains(input))
}

Here, you use the built-in TOTP type to generate a range of tokens and verify the range contains the user-submitted code. If not, you check the backup codes for the input.

Using multiple codes avoids issues where the server and client clocks are slightly out of sync. Instead of generating one code, passing range: 1 will return the current time’s code, along with the next and previous code.

The next step is to generate a new TwoFactorToken for a user. Add the following to the extension:

static func generate(for user: User) throws -> TwoFactorToken {
  // 1
  let data = Data([UInt8].random(count: 16)).base32EncodedString()
  // 2
  let key = SymmetricKey(data: Data(base32Encoded: data)!)
  // 3
  let hotp = HOTP(key: key, digest: .sha1, digits: .six)
  // 4
  let codes = (1...10).map { hotp.generate(counter: $0) }
  // 5
  return try TwoFactorToken(user.requireID(), data, codes)
}

This is what’s going on here:

  1. First generate some random data and encode it to a base-32 encoded string, making sure it only contains capital letters and numbers. This is important since it has to be URL encodable later on.
  2. Use the data to generate a SymmetricKey for the built-in TOTP and HOTP types.
  3. Create a HOTP instance with the same amount of digits as the TOTP instance from earlier.
  4. Use the HOTP to generate 10 backup codes.
  5. Create and return a new TwoFactorToken with the generated data and backup codes.

The stored key will also create the TOTP instance.

Retrieving OTP Tokens

Next up, add a function to create a TwoFactorToken.Public instance to return to users:

func asPublic() -> Public {
  let issuer = "DiningIn"
  let url = "otpauth://totp/\(self.user.username)?secret=\(self.key)&issuer=\(issuer)"
    return Public(
      backupCodes: self.backupTokens,
      key: self.key,
      label: self.user.username,
      issuer: issuer,
      url: url)
}
Note: This URL can generate a QR code to scan with Authy or Google Authenticator. Along with the URL, it’ll contain all parts that make up the URL and the backup tokens.

As the last step, add a static function to find a TwoFactorToken for a given User:

static func find(
  for user: User, 
  on db: Database
) -> EventLoopFuture<TwoFactorToken> {
  user.$twoFactorToken
    .query(on: db)
    .with(\.$user)
    .first()
    .unwrap(or: Abort(.internalServerError, reason: "No 2FA token found"))
}

This uses Fluent’s @Children property wrapper to query a user’s linked tokens, eager loads the user, and then unwraps and returns it. However, it doesn’t compile right yet, because a user doesn’t have a twoFactorToken property.

In User.swift, add the following to User under updatedAt:

@Children(for: \TwoFactorToken.$user)
var twoFactorToken: [TwoFactorToken]

Here, you complete the relationship between TwoFactorToken and User.

Validating OTP Tokens

While still in User.swift, add another @Field property under the passwordHash field to store whether or not a user has 2FA enabled:

@Field(key: "two_factor_enabled")
var twoFactorEnabled: Bool

Then, add the following line to the end of the User initializer to default twoFactorEnabled to false:

self.twoFactorEnabled = false

Adding the Migrations

Since these are database schema-related changes, the next step is to create.

Add a new file in the Migrations folder called CreateTwoFactorToken.swift with the following contents:

import Fluent

struct CreateTwoFactorToken: Migration {
  func prepare(on database: Database) -> EventLoopFuture<Void> {
    // 1
    let create = database.schema(TwoFactorToken.schema)
      .id()
      .field("user_id", .uuid, .required, .references(User.schema, "id"))
      .field("key", .string, .required)
      .unique(on: "user_id", "key")
      .field("backup_tokens", .array(of: .string), .required)
      .create()

    // 2    
    let update = database.schema(User.schema)
      .field("two_factor_enabled", .bool, .sql(.default(false)))
      .update()
    
    // 3
    return create.and(update).transform(to: ())
  }
  
  func revert(on database: Database) -> EventLoopFuture<Void> {
    // 4
    let delete = database.schema(TwoFactorToken.schema)
      .delete()
    
    // 5
    let update = database.schema(User.schema)
      .deleteField("two_factor_enabled")
      .update()

    // 6
    return delete.and(update).transform(to: ())
  }
}

This migration will do the following:

  1. To prepare, first create the schema that creates two_factor_tokens.
  2. Then, add two_factor_enabled to users.
  3. Using EventLoopFuture.and(_:), combine the two futures into a single future and transform it to Void.
  4. To revert, first create a delete step for two_factor_tokens.
  5. Then, create a step to remove two_factor_field from users.
  6. Again, using EventLoopFuture.and(_:), combine the two steps and transform to Void.

In configure.swift, add the migration. Below all other migrations, add the following:

app.migrations.add(CreateTwoFactorToken())

This ensures running the app will create the table and fields required for 2FA.

Setting up the Handshake

Enabling 2FA involves a handshake between client and server. This process consists of the following steps:

  1. The user will request for 2FA to be enabled.
  2. The API will generate a token and send it back in the response.
  3. The user will add the token to their 2FA app, like Authy or Google Authenticator.
  4. The user will send a code generated by Authy or Google Authenticator to the API.
  5. The API will verify the submitted code and enable 2FA for the user, if valid.

2FA Handshake Diagram

To facilitate this 2FA handshake, you’ll add two endpoints in UserController.swift: one to generate a 2FA token, and one to validate the 2FA flow.

Below boot, add the following:

// 1
fileprivate func getTwoFactorToken(
  _ req: Request
) throws -> EventLoopFuture<TwoFactorToken.Public> {
  // 2
  let user = try req.auth.require(User.self)
  // 3
  func _token() -> EventLoopFuture<TwoFactorToken.Public> {
    TwoFactorToken.find(for: user, on: req.db).map { $0.asPublic() }
  }
  // 4
  if user.twoFactorEnabled {
    // 5
    return _token()
  } else {
    // 6
    return user.$twoFactorToken.get(on: req.db)
      .flatMapThrowing { token -> TwoFactorToken? in
        // 7
        if let _ = token.first {
          return nil
        }
        // 8
        let token = try TwoFactorToken.generate(for: user)
        return token
      }
      // 9
      .optionalFlatMap { $0.save(on: req.db) }
      // 10
      .flatMap { _ in _token() }
  }
}

Here’s what’s going on here:

  1. Create a request handler returning TwoFactorToken.Public, which was created earlier.
  2. Get an instance of User by requiring authentication.
  3. Define a function that will find a TwoFactorToken for a user and return its public type.
  4. Check if the user already has 2FA enabled.
  5. If 2FA is already enabled, return the existing 2FA token’s information.
  6. Otherwise, if 2FA has not been set up, try to find an existing but unverified TwoFactorToken.
  7. If you find a token, return nil. This might seem counter intuitive, but it allows you to skip step 9.
  8. When you don’t find a token, generate a token and return it.
  9. Using optionalFlatMap(_:), save the generated token. This code will only execute when step 6 returns a non-nil value.
  10. Finally, use _token() to return the public version of a user’s token.

This takes care of the first three steps of the 2FA handshake.

Below getTwoFactorToken, add the following function that will take care of the last part of the handshake:

// 1
fileprivate func enableTwoFactor(
  _ req: Request
) throws -> EventLoopFuture<HTTPStatus> {
  // 2
  let user = try req.auth.require(User.self)
  // 3
  if user.twoFactorEnabled {
    return req.eventLoop.makeSucceededFuture(.ok)
  }
  // 4
  guard let t = req.headers.first(name: "X-Auth-2FA") else {
    throw Abort(.badRequest)
  }
  // 5
  return TwoFactorToken.find(for: user, on: req.db).flatMap { token in
    // 6
    guard token.validate(t, allowBackupCode: false) else {
      // 7
      return token.delete(force: true, on: req.db).flatMapThrowing {
        throw Abort(.unauthorized)
      }
    }
    // 8
    user.twoFactorEnabled = true
    return user.save(on: req.db).transform(to: .ok)
  }
}

Here’s what’s happening in the code above:

  1. Create a request handler returning a HTTP status code.
  2. Get an instance of the User type by requiring authentication.
  3. Check if the user already has 2FA enabled. If so, immediately return 200 OK.
  4. When 2FA isn’t enabled, attempt to get the submitted code from the X-Auth-2FA header. Return 400 Bad Request if the header is missing.
  5. Get the user’s 2FA token from the database.
  6. Make sure the code is valid. Disallow backup codes here to be 100% sure the submitted code checks that the HMAC key has been saved correctly.
  7. If the code isn’t valid, return 401 Unauthorized and delete the 2FA token from the database.
  8. If the code is valid, enable 2FA for the user, save it to the database and return 200 OK

In boot, add the following two lines below the tokenProtected line to register these two routes to the router:

tokenProtected.get("me", "twofactortoken", use: getTwoFactorToken)
tokenProtected.post("me", "enabletwofactor", use: enableTwoFactor)

Now it’s time to test the handshake!

Testing the Handshake

With all the routes in place, it’s time to test the handshake and make sure it can enable 2FA for a user. Build and run the app, and open the provided API collection in either Postman or Paw.

First, execute the Sign up request to create a user. Then, execute the Get 2FA Token request. The response will look something like this:

{
  "label": "NatanTheChef",
  "key": "VPGF37A5T3PF6PNY4Z3PLY65LQ",
  "backup_codes": [
      "428667",
      "841992",
      "172861",
      "873429",
      "110390",
      "817288",
      "587247",
      "936026",
      "592443",
      "530780"
  ],
  "url": "otpauth://totp/NatanTheChef?secret=VPGF37A5T3PF6PNY4Z3PLY65LQ&issuer=DiningIn",
  "issuer": "DiningIn"
}

Now for the fun stuff! To finish the handshake, you’ll have to add the token to an authenticator app. For the sake of testing, you can use Authy Desktop, which supports both macOS and Linux. This saves you from sending the key to your mobile device.

In your authenticator app, add a new entry and feed it the key from the response. It’ll start generating OTPs right away.

OTP in Authy

OTP Generation in Authy

Back in Postman or Paw, open the Enable 2FA request. Set the value of the X-Auth-2FA header to a current OTP code generated by your authenticator app, and execute the request.

If everything went well, you’ll see an empty 200 OK response. Well done!

Logging in With a 2FA Token

Now that users can enable 2FA for their account, it’s time to start enforcing it at the time of login. In UserController.swift, locate login(req:) and add the following below it:

fileprivate func loginWithTwoFactor(
  req: Request
) throws -> EventLoopFuture<NewSession> {
  // 1
  let user = try req.auth.require(User.self)
  
  // 2
  func createToken() throws -> Token {
    try user.createToken(source: .login)
  }
  // 3
  func createSession(_ token: Token) -> EventLoopFuture<NewSession> {
    return token.save(on: req.db).flatMapThrowing {
      NewSession(token: token.value, user: try user.asPublic())
    }
  }
  
  // 4
  if user.twoFactorEnabled {
    // 5
    guard let code = req.headers.first(name: "X-Auth-2FA") else {
      throw Abort(.partialContent)
    }
    // 6
    return TwoFactorToken.find(for: user, on: req.db)
      .flatMapThrowing { token in
        // 7
        guard token.validate(code) else { throw Abort(.unauthorized) }
      }
      // 8
      .flatMapThrowing(createToken)
      .flatMap(createSession)
  }
  // 9
  return try createSession(createToken())
}

This will achieve the following:

  1. Get an instance of User by requiring authentication
  2. Create a function that creates a new Token.
  3. Create a function that takes a token and wraps it in a session.
  4. Check if the logged-in user has 2FA enabled.
  5. With 2FA enabled, make sure there’s an OTP in the X-Auth-2FA header. If not, return 206: Partial Content indicating to your client they need to include the OTP.
  6. Retrieve the TwoFactorToken from the database.
  7. Validate the provided OTP against the stored key. If the code doesn’t pass, return 401 Unauthorized.
  8. Create a token, wrap it in a session and return it.
  9. If 2FA isn’t enabled for the user, immediately return a new session and token.

In boot, update the login route to use loginWithTwoFactor.

Now, it’s time to try it out. Build and run the app and open Postman or Paw. Find the Login request and execute it. It should return 206 Partial Content, indicating an OTP is necessary. Add a valid OTP to the X-Auth-2FA header for the request and execute it again. You should see the expected token response like before.

Extracting 2FA Into Middleware

The entire 2FA flow is working now. Users can enable 2FA on their accounts and exchange valid OTPs for a login token. However, currently, the login route handler handles the login logic. The 2FA code would be reusable if extracted into an Authenticator, like the existing ModelAuthenticatable and ModelTokenAuthenticatable.

Back in Xcode, in the App directory, create a new subfolder called Middleware. Inside, create a new file called OTPAuthenticatable.swift with the following content:

import Fluent
import Vapor

// 1
protocol OTPToken {
  func validate(_ input: String, allowBackupCode: Bool) -> Bool
}

// 2
protocol TwoFactorAuthenticatable: ModelAuthenticatable {
  // 3
  associatedtype _Token: Model & OTPToken
  // 4
  static var twoFactorEnabledKey: KeyPath<Self, Field<Bool>> { get }
  // 5
  static var twoFactorTokenKey: KeyPath<Self, Children<_Token>> { get}
}

extension TwoFactorAuthenticatable {
  // 6
  var _$twoFactorEnabled: Field<Bool> {
    self[keyPath: Self.twoFactorEnabledKey]
  }

  var _$twoFactorToken: Children<_Token> {
    self[keyPath: Self.twoFactorTokenKey]
  }

  var _$username: Field<String> {
    self[keyPath: Self.usernameKey]
  }

  // 9
  static func authenticator(database: DatabaseID? = nil) -> Authenticator {
    return TwoFactorUserAuthenticationMiddleware<Self>(database: database)
  }
}

This creates two new protocols with the following functionality:

  1. OTPToken only defines validate(_:allowBackupCode:) as a requirement. You use this later to validate the OTP.
  2. TwoFactorAuthenticatable builds on top of Vapor’s ModelAuthenticatable but adds a few extra requirements.
  3. The first requirement is the associated type, _Token, which has to conform to Model and OTPToken.
  4. The second requirement is a static keypath pointing toward a Boolean property which indicates whether or not 2FA is enabled.
  5. The final requirement is a static keypath pointing toward _Token.
  6. In an extension, three non-static properties will use the static keypaths to make the actual values available.
  7. Lastly, implement a static function to create an Authenticator that you’ll use in the route handler.

Implementing Middleware

Before this will work, you must implement TwoFactorUserAuthenticationMiddleware. At the bottom of OTPAuthenticatable.swift add the following:

// 1
struct TwoFactorUserAuthenticationMiddleware<T>:
  Authenticator where T: TwoFactorAuthenticatable {
  let database: DatabaseID?
  
  func respond(
    to request: Request,
    chainingTo next: Responder
  ) -> EventLoopFuture<Response> {
    // 2
    guard let basic = request.headers.basicAuthorization else {
      return next.respond(to: request)
    }
    
    // 3
    return T.query(on: request.db(self.database))
      .filter(\._$username == basic.username)
      .first()
      .flatMap { user in
        // 4
        guard let user = user else {
          return next.respond(to: request)
        }
        
        // 5
        if let twoFactorEnabled = user._$twoFactorEnabled.value,
           twoFactorEnabled {
          // 6
          guard let twoFactorHeader = request.headers.first(
                  name: "X-Auth-2FA") else {
            return request.eventLoop.makeFailedFuture(Abort(.partialContent))
          }
          // 7
          return user._$twoFactorToken
            .query(on: request.db(self.database))
            .first()
            .optionalFlatMapThrowing { token in
              // 8
              if try user.verify(password: basic.password) &&
                  token.validate(twoFactorHeader, allowBackupCode: true) {
                request.auth.login(user)
              }
              // 9
            }.flatMap { _ in next.respond(to: request) }
        }
        
        do {
          // 10
          if try user.verify(password: basic.password) {
            request.auth.login(user)
          }
        } catch { }
        // 11
        return next.respond(to: request)
      }
  }
}

There’s a lot going on here, so here’s a breakdown:

  1. First, declare a new Authenticator that’s generic over a TwoFactorAuthenticatable type.
  2. In the middleware’s respond(to:chainingTo:) function, first make sure the required basic authentication data is present. If not, pass the request to the next responder.
  3. Using TwoFactorAuthenticatable, find an instance matching the username from the Basic authentication data.
  4. If there’s no user, pass the request to the next responder.
  5. When a user is found, check if 2FA is enabled.
  6. Get the provided OTP from the X-Auth-2FA header. Return 206 Partial Content if the header is missing.
  7. Use the user’s linked two-factor tokens to find an instance of OTPToken.
  8. If an instance is found, verify the user’s password credentials match and validate the supplied OTP matches. If so, log the user in.
  9. Pass the request to the next responder. Due to using optionalFlatMapThrowing(_:), the verification process will only happen if an OTPToken was found.
  10. If 2FA isn’t enabled for the user, verify basic authentication credentials and log the user in if they’re valid.
  11. Finally, pass the request to the next responder.

This will make it a lot easier to implement 2FA in future projects!

Getting Middleware Working

Now it’s time to connect the dots and get the middleware working. In 2FAToken.swift, conform TwoFactorToken to OTPToken. The required validate(_:allowBackupCode:) function is already implemented, so no further action is necessary.

Next, in User.swift, find the extension conforming User to ModelAuthenticatable. Replace the entire extension with the following:

extension User: TwoFactorAuthenticatable {
  // 1
  static let usernameKey = \User.$username
  static let passwordHashKey = \User.$passwordHash
  // 2
  static let twoFactorEnabledKey = \User.$twoFactorEnabled
  static let twoFactorTokenKey = \User.$twoFactorToken

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

This will conform User to TwoFactorAuthenticatable as follows:

  1. First, keep the properties required by ModelAuthenticatable, since TwoFactorAuthenticatable extends it.
  2. Second, add the properties specific to TwoFactorAuthenticatable. For the twoFactorEnabledKey, use the $twoFactorEnabled Fluent field, and for twoFactorTokenKey, use the $twoFactorToken field.
  3. Finally, implement ModelAuthenticatable‘s verify for basic password authentication.

Because of the magic of Swift, all routes previously using User‘s ModelAuthenticatable feature set now automatically also get the 2FA protection.

As the final step, in UserController.swift in boot(boot:), revert the login route to use the original login handler again, instead of loginWithTwoFactor. The latter is no longer required since the middleware will handle it now.

With everything set up, build and run the app and open Postman or Paw. Again, find the Login request and execute it. If your previous OTP is still in the headers, it should respond with 401 Unauthorized. Before putting in a valid OTP, remove the header and make sure the API returns 206 Partial Content.

Now, put in a valid OTP and the API should once again give back a token — this time, without the UserController even knowing about 2FA. Awesome!

Where to Go From Here?

You can download the final project using the Download Materials button at the top or bottom of this page.

In this tutorial, you learned about creating one-time passwords, added 2FA to your app and extracted 2FA into reusable middleware. Great job!

To learn more about the cryptography behind OTPs, you can read about HMAC, SHA-1 and SHA-2 on Wikipedia.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

5/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments