Securing an iOS App with 2FA Using Nexmo Verify SDK

Nowadays, developers are opting for phone verification over email verification as it is effective in reducing spam accounts. Applications requiring higher security (such as banking apps), combine phone verification with an email/username login to implement 2 factor authentication. Adding phone verification to your app doesn't need to be complex. This tutorial will show you how to use the Nexmo Verify SDK for iOS in a "To-Do List" application that uses Parse for a backend.

First, include the Verify SDK in your project folder as a submodule by running the command: 

git submodule add https://github.com/nexmo/verify-ios-sdk.git. The Verify SDK is currently in the process of being added onto CocoaPods, so this is the current method to add the SDK to your project. Once complete, you should see the ‘verify-ios-sdk’ folder from your project folder. Open up the newly added folder & drag the VerifyIosSdk.xcodeproj to the root of your project in Xcode. Click the VerifyIosSdk project & check that the deployment target is set to the same as your project.  

Add your Nexmo Application ID & Shared Secret Key as private variables to your AppDelegate.swift (this is the entry point for the application & what handles special UIApplication states).

You will then initialize the Nexmo Client inside the didFinishLaunchingWithOptions function of the file (you can obtain these for your app in the Nexmo Dashboard in the Products tab, and click Verify SDK).

private var appID = "YOUR-NEXMO-APP-ID"
private var sharedSecret = "YOUR-NEXMO-SHARED-SECRET"

NexmoClient.start(applicationId: appID, sharedSecretKey: sharedSecret)

Now, switching over to the Parse Dashboard, add a row to your existing _User class to add a phone number for each user. Make sure the new row is of type ‘String’. To ensure each user signs up with a phone number that has not been taken, you can include a query that checks for previous entries of signed up users and return an error if a same one is found. As shown below, this is done inside the function signup() and is being called inside the signUpButton’s action function. The Verify SDK takes care of checking for valid numbers provided by the user using Nexmo’s Number Insight API.

@IBAction func signupButton(sender: AnyObject) {
       signup()
}

func signup() {
       let alert = UIAlertView()
       if (self.phoneNumber.text!.isEmpty || self.password.text!.isEmpty || self.userName.text!.isEmpty) {
           alert.title = "Invalid Credentials"
           alert.message = "Make sure to fill out all the required fields."
           alert.addButtonWithTitle("Back")
           alert.show()
       }
       else {
           let query = PFQuery(className: "_User")
           query.whereKey("phoneNumber", equalTo: phoneNumber.text!)
           query.findObjectsInBackgroundWithBlock {
               (objects: [PFObject]?, error: NSError?) in
               if error == nil {
                   if (objects!.count > 0){
                       alert.title = "Phone Number Error"
                       alert.message = "This phone number is associated with another account."
                       alert.addButtonWithTitle("Back")
                       alert.show()
                   }
                   else {
                       self.performSegueWithIdentifier("verify", sender:self)
                   }
               }
           }
       }
}

Now that you have completed the signup phone number check logic for Parse, add a view controller that will start the user verification process and check the pin code supplied by the end user.

Here I added outlets for the pinCode and an action for the ‘Check Pin’ button:

Add a segue identifier from the SignupViewController to the newly created view controller and send the user’s info (in process of signing up) to VerifyViewController:

let verifyVC = segue.destinationViewController as! VerifyViewController;
verifyVC.phoneNumber = phoneNumber.text
verifyVC.userName = userName.text
verifyVC.passWord = password.text

The SDK allows the user to use a standard UX provided in the Verify SDK which has a list of countries with their corresponding country codes & many added benefits including easy customization of the verification process. I happened to design my own UI for this app (to show how one would implement the SDK with your own UI) and used the onError method to throw specific errors to the user for number checking and other features taken care by the Verify SDK. 

In the newly added view controller (inside the viewDidLoad method), use the getVerifiedUser() method to start the Verification process by VerifyClient. Within this function call, inside the onUserVerified method, add the Parse sign up logic. This ensures that once the user is verified, only then will the app complete the signup process and send the newly added user’s information to our database on Parse. Include the username, password, and phone number the user has just verified which then completes the Parse’s user signup process:

VerifyClient.getVerifiedUser(countryCode: "US", phoneNumber: phoneNumber, onVerifyInProgress: {
           }, onUserVerified: {
               let user = PFUser()
               user.username = self.userName
               user.password = self.passWord
               user["phoneNumber"] = self.phoneNumber
               user.signUpInBackgroundWithBlock {
                   (succeeded: Bool, error: NSError?) -> Void in
                   if let error = error {
                       let errorString = error.userInfo["error"] as? NSString
                       let alert = UIAlertView()
                       alert.title = "Sign Up Error"
                       alert.message = errorString?.capitalizedString
                       alert.addButtonWithTitle("Back")
                       alert.show()
                   }
                   else {
                       self.performSegueWithIdentifier("verifiedUser", sender: self)
                   }
               }

Inside the onError() method, handle the error with a switch statement and with several cases. For example, you can throw alerts for cases such as: Invalid Phone Number, Wrong Pin Code, Invalid Credentials, and more:

          }, onError: { verifyError in
               switch (verifyError) {
                   case .INVALID_NUMBER:
                       UIAlertView(title: "Invalid Phone Number", message: "The phone number you entered is invalid.", delegate: nil, cancelButtonTitle: "Oh, gosh darnit!").show()
                   case .INVALID_PIN_CODE:
                       UIAlertView(title: "Wrong Pin Code", message: "The pin code you entered is invalid.", delegate: nil, cancelButtonTitle: "Whoops!").show()
                   case .INVALID_CODE_TOO_MANY_TIMES:
                       UIAlertView(title: "Invalid code too many times", message: "You have entered an invalid code too many times, verification process has stopped..", delegate: nil, cancelButtonTitle: "Okay").show()
                   case .INVALID_CREDENTIALS:
                   UIAlertView(title: "Invalid Credentials", message: "Having trouble connecting to your account. Please check your app key and secret.", delegate: nil, cancelButtonTitle: "Sure thing.").show()
                   case .USER_EXPIRED:
                   UIAlertView(title: "User Expired", message: "Verification for current use expired (usually due to timeout), please start verification again.", delegate: nil, cancelButtonTitle: "Okay").show()
                   case .USER_BLACKLISTED:
                   UIAlertView(title: "User Blacklisted", message: "Unable to verify this user due to blacklisting!", delegate: nil, cancelButtonTitle: "Whoa...").show()
                   case .QUOTA_EXCEEDED:
                       UIAlertView(title: "Quota Exceeded", message: "You do not have enough credit to complete the verification.", delegate: nil, cancelButtonTitle: "Okay").show()
                   case .SDK_REVISION_NOT_SUPPORTED:
                       UIAlertView(title: "SDK Revision too old", message: "This SDK revision is not supported anymore!", delegate: nil, cancelButtonTitle: "Okay").show()
                   case .OS_NOT_SUPPORTED:
                       UIAlertView(title: "iOS version not supported", message: "This iOS version is not supported", delegate: nil, cancelButtonTitle: "Okay").show()
                   case .VERIFICATION_ALREADY_STARTED:
                       UIAlertView(title: "Verification already started", message: "A verification is already in progress!", delegate: nil, cancelButtonTitle: "Oh, okay").show()
                   default: break
               }
})

Now that users are verified on signup, you need to complete a verification on login to finish the 2FA process. The steps are the basically the same as above with a few modifications. In your login view controller, there should be a basic set-up such as username, password. Along with this, add a Login button and a Signup button (segues to the newly added signup view controller). For example, my login screen looks like this:

If the user-entered combination does not match, l throw an alert view with an invalid credentials error. Create a function for Parse’s user login logic (such as login()) and call the function in the login button action.

func login() {
       PFUser.logInWithUsernameInBackground(userName.text!, password:password.text!) {
           (user: PFUser?, error: NSError?) -> Void in
           if user != nil {
               self.currentUser  = PFUser.currentUser()
               print("Success")
           }
           else {
               let alert = UIAlertView()
               alert.title = "Login Error"
               alert.message = "Please re-enter your credentials or sign up below."
               alert.addButtonWithTitle("Back")
               alert.show()
           }
       }
}

@IBAction func loginButton(sender: AnyObject) {
       login()
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject!) {
       if (segue.identifier == "verifyLogin") {
           let loggedInVC = segue.destinationViewController as! VerifyLoginViewController;
           loggedInVC.currentUser = self.currentUser
       }
}

Again (as I am using my own UI), create a new View Controller (ex: VerifyLoginViewController) that controls the phone verification process. Add a segue from the LoginViewController and send the current user’s information to the newly created VerifyLoginViewController.

Add a text field for pin code (IBOutlet), a Verify button (IBAction) that checks the code the user has supplied, and a variable for the current user of type PFUser sent via the segue from the previous view controller. 

var currentUser = PFUser()    
@IBOutlet weak var pinCode: UITextField!
@IBAction func verifyButton(sender: AnyObject) {
       VerifyClient.checkPinCode(pinCode.text!)
}

Inside the viewDidLoad method of the newly added view controller, use the getVerifiedUser() method to start the Verification process by VerifyClient.

override func viewDidLoad() {
       super.viewDidLoad()
       VerifyClient.getVerifiedUser(countryCode: "US", phoneNumber: currentUser["phoneNumber"].description,
           onVerifyInProgress: {
               print("Verification Begun")
           }, onUserVerified: {
               self.performSegueWithIdentifier("verifiedLogin", sender:self)
      //User is verified, segue to actual app (ex: segues to the user’s To Do List)
       }, onError: { verifyError in
           self.performSegueWithIdentifier("error", sender: self)
       })
}

And that’s it! That adds the user to the backend on Parse including a phone number that has been verified using the Verify SDK. 

Adding 2FA to an existing app or Web site is a critical security measure to take nowadays. Fortunately, with the Nexmo Verify SDK, it makes the process much more smooth & simple.    

Some things to note:

I used the Verify SDK to add phone verification on login and when adding an item to the user’s ToDo list. This allows users to access their list of ToDos on other devices & still be able to stay secure when adding items to their list of ToDos. 

Start the phone verification when the AddViewController view loads. Then, check the pincode inside the Add Button action and add the logic to save the PFObject (ToDo in this case) inside the onUserVerified method of the function call.

override func viewDidLoad() {
       super.viewDidLoad()
       VerifyClient.getVerifiedUser(countryCode: "US", phoneNumber: PFUser.currentUser()!["phoneNumber"].description,
           onVerifyInProgress: {
           }, onUserVerified: {
               let toDoItem = PFObject(className:"ToDo")
               toDoItem["user"] = PFUser.currentUser()!
               if self.item.text!.isEmpty == false {
                   toDoItem["todoItem"] = self.item.text
                   toDoItem.saveInBackgroundWithBlock {
                       (success: Bool, error: NSError?) -> Void in
                       print("Object has been saved.")
                       self.performSegueWithIdentifier("addToDoItem", sender:self)
                   }
               }
               else {
                   let alert = UIAlertView()
                   alert.title = "Invalid Entry"
                   alert.message = "Make sure to add a To-Do item."
                   alert.addButtonWithTitle("Back")
                   alert.show()
                   self.performSegueWithIdentifier("invalidEntry", sender:self)
               }
           },
           onError: { verifyError in
               //Add error logic if desired, since user has been verified previously on signup & each time on login - not necessary)
       })
}

@IBAction func addButton (sender: AnyObject) {
    VerifyClient.checkPinCode(pinCode.text!)
}

In addition, if you have an existing application using Parse and try to add 2FA to your solution, your backend should be structured as described above by adding a String column named for the user’s phone number and ensuring all existing users have a value for phone number. Parse allows fields to be changed easily on the dashboard.

Now you have a complete 2FA app with an extra layer of security through phone verification on a specific user action. This type of use case can extend in various forms throughout various markets from ToDo list applications when adding or deleting an item, banking apps that verify users with a temporary text passcode when withdrawing large amounts of cash, and more. Phone verification adds the extra layer user authentication that helps keeps users and their data safe.

Sidharth Sharma Sidharth is an iOS Developer at Nexmo. He talks Swift, C#, and enjoys learning other programming languages as a hobby. He is also a part of the DevRel team at Nexmo and loves to attend hackathons, argue about tabs vs spaces, & converse with passion developers about code.

Comments