How to Add Two-Factor Authentication with Swift and Vapor
Published on November 24, 2020

Introduction

Two-factor authentication (2FA) is when you use two different things to verify your identity. Usually, something you know, like a password, paired with a verification code from a physical device like a phone.

This tutorial will cover how to implement a verification token system with the Vonage Verify API and Vapor. Once finished, you can test the system with a SwiftUI application.

Prerequisites

  • Xcode 12 and Swift 5 or greater.

  • Vapor 4.0 installed on your machine.

  • ngrok for exposing your local machine to the internet.

DT API Account

To complete this tutorial, you will need a DT API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the DT API Dashboard.

Create a Vapor Project

You can create a Vapor project using the new project command vapor new SwiftVerify -n in your terminal. Once the command has finished change directory into the folder, it created for you using cd SwiftVerify. Now you can open the project in Xcode using vapor xcode.

Once Xcode opens, it will start downloading the dependencies that Vapor relies on using Swift Package Manager (SPM). To view the dependencies, you can open the Package.swift file.

Create the Model Structs

A major benefit of using Vapor is that you can lean on the Swift language's type-safety. You can model inputs and outputs to your server using structs that conform to the Codable protocol; Vapor has a protocol called Content for this.

Start by creating a struct called Vonage to house all the model code. Create a new file under Sources > App called VonageClient.swift. In the new file, create the Vonage struct:

public struct Vonage {
    private let apiKey: String
    private let apiSecret: String
    
    public init(apiKey: String, apiSecret: String) {
        self.apiKey = apiKey
        self.apiSecret = apiSecret
    }
}

The struct is initialized with the API key and secret from your Vonage API account and stores them as local properties for use later. You will be creating two API endpoints in the tutorial, one to request a verification code, and one to check if the code was correct. Create two more structs within the Vonage struct for this:

public struct RequestVerificationBody: Content {
    let number: String
    let brand: String = "SwiftVerify"
    var apiKey: String?
    var apiSecret: String?
    
    init(body: RequestVerificationBody, apiKey: String, apiSecret: String) {
        self.number = body.number
        self.apiKey = apiKey
        self.apiSecret = apiSecret
    }
    
    private enum CodingKeys: String, CodingKey {
        case number
        case brand
        case apiKey = "api_key"
        case apiSecret = "api_secret"
    }
}
    
public struct CheckVerificationBody: Content {
    let requestID: String
    let code: String
    var apiKey: String?
    var apiSecret: String?
    
    init(body: CheckVerificationBody, apiKey: String, apiSecret: String) {
        self.requestID = body.requestID
        self.code = body.code
        self.apiKey = apiKey
        self.apiSecret = apiSecret
    }
    
    private enum CodingKeys: String, CodingKey {
        case requestID = "request_id"
        case code
        case apiKey = "api_key"
        case apiSecret = "api_secret"
    }
}

These structs have a dual purpose; to use the input into the server and its output. The non-optional properties and properties without a default value, for example, number on RequestVerificationBody are supplied when you make a request to the server.

The custom initializer takes in the version of the struct from the request to the server, enrich it with remaining properties then use it to make a call to the Vonage APIs. The Vonage APIs expect fields in snake case, so the structs have the CodingKeys enum to map their property names to their snake case equivalent.

Create a Verification Request

You need to make a call to the Verify API to create a verification request. The endpoint you want to call to create a verification request is /verify. Create a function in the Vonage struct to do so:

public func requestVerification(with body: RequestVerificationBody, client: Client) -> EventLoopFuture<clientresponse> {
    return client.post(URI(scheme: "https", host: "api.nexmo.com", path: "/verify/json")) { req in
        try req.content.encode(RequestVerificationBody(body: body, apiKey: apiKey, apiSecret: apiSecret), as: .json)
    }
}
</clientresponse>

The function takes the body of the request made to your server and a Client. Vapor's Client API allows you to make external HTTP calls. Before the post request gets sent, the body becomes encoded with an enriched RequestVerificationBody struct. The function returns an EventLoopFuture which is a generic type that references a value that is not available yet, in your case, the response from the post request.

The next step is to define the route, which is the endpoint on your server that will call the above function. Open routes.swift, create an instance of the Vonage struct and define the new route:

func routes(_ app: Application) throws {
    let client = Vonage(apiKey: "API_KEY", apiSecret: "API_SECRET")
    
    app.post("request") { req -> EventLoopFuture<clientresponse> in
        let body = try req.content.decode(Vonage.RequestVerificationBody.self)
        return client.requestVerification(with: body, client: req.client)
    }
}
</clientresponse>

Replace API_KEY and API_SECRET with your credentials from the Vonage API dashboard. In a production environment, you can use Vapor's Environment API to avoid exposing your credentials.

When the /request endpoint on your server receives a request, it will decode the body of that request into a RequestVerificationBody struct, then use it to call the function you created earlier. By default workflow 1 is used, you can add a property to the RequestVerificationBody, with a coding key mapping to workflow_id to change this.

The result of the call will have a status property; when this is 0, it means the action has been successful. It will also include a request_id; this is what is used to check the code is valid.

Check the Code

Checking if the code is valid is a very similar process. Add a function to the Vonage struct to call the Verify API, this time making a post request to /verify/check with a CheckVerificationBody struct:

public func checkVerification(with body: CheckVerificationBody, client: Client) -> EventLoopFuture<clientresponse> {
    return client.post(URI(scheme: "https", host: "api.nexmo.com", path: "/verify/check/json")) { req in
        try req.content.encode(CheckVerificationBody(body: body, apiKey: apiKey, apiSecret: apiSecret), as: .json)
    }
}
</clientresponse>

Then add the route in routes.swift:

app.post("check") { req -> EventLoopFuture<clientresponse> in
    let body = try req.content.decode(Vonage.CheckVerificationBody.self)
    return client.checkVerification(with: body, client: req.client)
}
</clientresponse>

Similarly to the earlier request route, the result of the call will have a status property with 0 meaning success.

Test Your Server

Now that your routes are defined, you can build and run (CMD + R) your server. Once complete, your server will be running locally on port 8080.

Terminal output when running projectTerminal output when running project

To expose this to the internet, you can use ngrok. In your terminal run ngrok http 8080. A public URL is generated which forwards calls to your local machine.

ngrok terminal outputngrok terminal output

Now that your server is available on the internet, you can make calls to it, to test your server, you can use the test application. Either download the project or clone with your terminal using git clone git@github.com:nexmo-community/swiftui-two-factor-app.git.

Once downloaded, open the project in Xcode. In the VerifyModel.swift file, replace the BASE_URL string with the forwarding URL from ngrok, then build and run (CMD + R). You can enter your phone number, and it will receive a text which you can enter to verify your phone number!

Test app screenshotsTest app screenshots

What Next?

You can find the completed project on GitHub. There is more you can do with the Verify API such as changing the workflow event timings or using it to authorize payments. Learn more on developer.nexmo.com.

Abdul AjetunmobiSenior Developer Advocate

Abdul is a Developer Advocate for Vonage. He has a background working in consumer products as an iOS Engineer. In his spare time, he enjoys biking, listening to music and mentoring those who are just beginning their journey in tech

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.