Two-Factor Authentication With Vapor

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

Leave a rating/review
Download materials
Save for later
Share

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.