Two-Factor Authentication With Vapor
Learn how to increase the account security of your using two-factor authentication with Vapor. By Jari Koopman.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Two-Factor Authentication With Vapor
30 mins
- Getting Started
- Understanding Two-Factor Authentication
- Hash Based One Time Passwords
- Time Based One Time Passwords
- Setting up 2FA
- OTP Tokens
- Generating OTP Tokens
- Retrieving OTP Tokens
- Validating OTP Tokens
- Adding the Migrations
- Setting up the Handshake
- Testing the Handshake
- Logging in With a 2FA Token
- Extracting 2FA Into Middleware
- Implementing Middleware
- Getting Middleware Working
- Where to Go From Here?
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.
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.
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:
- 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.
- Use the data to generate a
SymmetricKey
for the built-inTOTP
andHOTP
types. - Create a
HOTP
instance with the same amount of digits as theTOTP
instance from earlier. - Use the HOTP to generate 10 backup codes.
- 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)
}
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:
- To prepare, first create the schema that creates
two_factor_tokens
. - Then, add
two_factor_enabled
tousers
. - Using
EventLoopFuture.and(_:)
, combine the two futures into a single future and transform it toVoid
. - To revert, first create a delete step for
two_factor_tokens
. - Then, create a step to remove
two_factor_field
fromusers
. - Again, using
EventLoopFuture.and(_:)
, combine the two steps and transform toVoid
.
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:
- The user will request for 2FA to be enabled.
- The API will generate a token and send it back in the response.
- The user will add the token to their 2FA app, like Authy or Google Authenticator.
- The user will send a code generated by Authy or Google Authenticator to the API.
- The API will verify the submitted code and enable 2FA for the user, if valid.
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:
- Create a request handler returning
TwoFactorToken.Public
, which was created earlier. - Get an instance of
User
by requiring authentication. - Define a function that will find a
TwoFactorToken
for a user and return its public type. - Check if the user already has 2FA enabled.
- If 2FA is already enabled, return the existing 2FA token’s information.
- Otherwise, if 2FA has not been set up, try to find an existing but unverified
TwoFactorToken
. - If you find a token, return
nil
. This might seem counter intuitive, but it allows you to skip step 9. - When you don’t find a token, generate a token and return it.
- Using
optionalFlatMap(_:)
, save the generated token. This code will only execute when step 6 returns a non-nil
value. - 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:
- Create a request handler returning a HTTP status code.
- Get an instance of the
User
type by requiring authentication. - Check if the user already has 2FA enabled. If so, immediately return
200 OK
. - When 2FA isn’t enabled, attempt to get the submitted code from the
X-Auth-2FA
header. Return400 Bad Request
if the header is missing. - Get the user’s 2FA token from the database.
- 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.
- If the code isn’t valid, return
401 Unauthorized
and delete the 2FA token from the database. - 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 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:
- Get an instance of
User
by requiring authentication - Create a function that creates a new
Token
. - Create a function that takes a token and wraps it in a session.
- Check if the logged-in user has 2FA enabled.
- With 2FA enabled, make sure there’s an OTP in the
X-Auth-2FA
header. If not, return206: Partial Content
indicating to your client they need to include the OTP. - Retrieve the
TwoFactorToken
from the database. - Validate the provided OTP against the stored key. If the code doesn’t pass, return
401 Unauthorized
. - Create a token, wrap it in a session and return it.
- 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:
-
OTPToken
only definesvalidate(_:allowBackupCode:)
as a requirement. You use this later to validate the OTP. -
TwoFactorAuthenticatable
builds on top of Vapor’sModelAuthenticatable
but adds a few extra requirements. - The first requirement is the associated type,
_Token
, which has to conform toModel
andOTPToken
. - The second requirement is a static keypath pointing toward a Boolean property which indicates whether or not 2FA is enabled.
- The final requirement is a static keypath pointing toward
_Token
. - In an extension, three non-static properties will use the static keypaths to make the actual values available.
- 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:
- First, declare a new
Authenticator
that’s generic over aTwoFactorAuthenticatable
type. - 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. - Using
TwoFactorAuthenticatable
, find an instance matching the username from the Basic authentication data. - If there’s no user, pass the request to the next responder.
- When a user is found, check if 2FA is enabled.
- Get the provided OTP from the
X-Auth-2FA
header. Return206 Partial Content
if the header is missing. - Use the user’s linked two-factor tokens to find an instance of
OTPToken
. - If an instance is found, verify the user’s password credentials match and validate the supplied OTP matches. If so, log the user in.
- Pass the request to the next responder. Due to using
optionalFlatMapThrowing(_:)
, the verification process will only happen if anOTPToken
was found. - If 2FA isn’t enabled for the user, verify basic authentication credentials and log the user in if they’re valid.
- 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:
- First, keep the properties required by
ModelAuthenticatable
, sinceTwoFactorAuthenticatable
extends it. - Second, add the properties specific to
TwoFactorAuthenticatable
. For thetwoFactorEnabledKey
, use the$twoFactorEnabled
Fluent field, and for twoFactorTokenKey, use the$twoFactorToken
field. - Finally, implement
ModelAuthenticatable
‘sverify
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!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more