How To Secure iOS User Data: The Keychain and Biometrics — Face ID or Touch ID

Learn how to use the keychain and biometrics to secure your app and use Face ID or Touch ID. By Tim Mitra.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Putting Touch ID to Work

Open, TouchIDAuthentication.swift and add the following variable below context:

var loginReason = "Logging in with Touch ID"

The above provides the reason the application is requesting authentication. It will display to the user on the presented dialog.

Next, add the following method to the bottom of BiometricIDAuth to authenticate the user:

func authenticateUser(completion: @escaping () -> Void) { // 1
  // 2
  guard canEvaluatePolicy() else {
    return
  }

  // 3
  context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
    localizedReason: loginReason) { (success, evaluateError) in
      // 4
      if success {
        DispatchQueue.main.async {
          // User authenticated successfully, take appropriate action
          completion()
        }
      } else {
        // TODO: deal with LAError cases
      }
  }
}

Here’s what’s going on in the code above:

  1. authenticateUser(completion:) takes a completion handler in the form of a closure.
  2. You’re using canEvaluatePolicy() to check whether the device is capable of biometric authentication.
  3. If the device does support biometric ID, you then use evaluatePolicy(_:localizedReason:reply:) to begin the policy evaluation — that is, prompt the user for biometric ID authentication. evaluatePolicy(_:localizedReason:reply:) takes a reply block that is executed after the evaluation completes.
  4. Inside the reply block, you are handling the success case first. By default, the policy evaluation happens on a private thread, so your code jumps back to the main thread so it can update the UI. If the authentication was successful, you will call the segue that dismisses the login view.

You’ll come back and deal with errors in little while.

Open, LoginViewController.swift, locate touchIDLoginAction(_:) and replace it with the following:

@IBAction func touchIDLoginAction() {    
  touchMe.authenticateUser() { [weak self] in
    self?.performSegue(withIdentifier: "dismissLogin", sender: self)
  }
}

If the user is authenticated, you can dismiss the Login view.

Go ahead and build and run to see if all’s well.

Dealing with Errors

Wait! What if you haven’t set up biometric ID on your device? What if you are using the wrong finger or are wearing a disguise? Let’s deal with that.

An important part of Local Authentication is responding to errors, so the framework includes an LAError type. There also is the possibility of getting an error from the second use of canEvaluatePolicy.

You’ll present an alert to show the user what has gone wrong. You will need to pass a message from the TouchIDAuth class to the LoginViewController. Fortunately you have the completion handler that you can use it to pass the optional message.

Open, TouchIDAuthentication.swift and update the authenticateUser method.

Change the signature to include an optional message you’ll pass when you get an error.

func authenticateUser(completion: @escaping (String?) -> Void) {

Next, find the // TODO: and replace it with the following:

// 1
let message: String
            
// 2
switch evaluateError {
// 3
case LAError.authenticationFailed?:
  message = "There was a problem verifying your identity."
case LAError.userCancel?:
  message = "You pressed cancel."
case LAError.userFallback?:
  message = "You pressed password."
case LAError.biometryNotAvailable?:
  message = "Face ID/Touch ID is not available."
case LAError.biometryNotEnrolled?:
  message = "Face ID/Touch ID is not set up."
case LAError.biometryLockout?:
  message = "Face ID/Touch ID is locked."
default:
  message = "Face ID/Touch ID may not be configured"
}
// 4
completion(message)

Here’s what’s happening

  1. Declare a string to hold the message.
  2. Now for the “failure” cases. You use a switch statement to set appropriate error messages for each error case, then present the user with an alert view.
  3. If the authentication failed, you display a generic alert. In practice, you should really evaluate and address the specific error code returned, which could include any of the following:
    • LAError.biometryNotAvailable: the device isn’t Face ID/Touch ID-compatible.
    • LAError.passcodeNotSet: there is no passcode enabled as required for Touch ID
    • LAError.biometryNotEnrolled: there are no face or fingerprints stored.
    • LAError.biometryLockout: there were too many failed attempts.
  4. Pass the message in the completion closure.

iOS responds to LAError.passcodeNotSet and LAError.biometryNotEnrolled on its own with relevant alerts.

There’s one more error case to deal with. Add the following inside the else block of the guard statement, just above return.

completion("Touch ID not available")

The last thing to update is the success case. That completion should contain nil, indicating that you didn’t get any errors. Inside the first success block add the nil.

completion(nil)

Once you’ve completed these changes your finished method should look like this:

func authenticateUser(completion: @escaping (String?) -> Void) {
    
  guard canEvaluatePolicy() else {
    completion("Touch ID not available")
    return
  }
    
  context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
    localizedReason: loginReason) { (success, evaluateError) in
      if success {
        DispatchQueue.main.async {
          completion(nil)
        }
      } else {
                              
        let message: String
                              
        switch evaluateError {
        case LAError.authenticationFailed?:
          message = "There was a problem verifying your identity."
        case LAError.userCancel?:
          message = "You pressed cancel."
        case LAError.userFallback?:
          message = "You pressed password."
        case LAError.biometryNotAvailable?:
          message = "Face ID/Touch ID is not available."
        case LAError.biometryNotEnrolled?:
          message = "Face ID/Touch ID is not set up."
        case LAError.biometryLockout?:
          message = "Face ID/Touch ID is locked."
        default:
          message = "Face ID/Touch ID may not be configured"
        }
          
        completion(message)
      }
  }
}
Note: When you compile this new error handling, you will see three warnings complaining of using deprecated constants. This is due to a combination of the way Apple added support for Face ID and the way Swift imports Objective-C header files. There are some potential workarounds, but they are much less “Swift-like”. Since Apple is aware of the issue and plans to fix it at a future date, the cleaner code is presented here.

Open LoginViewController.swift and update the touchIDLoginAction(_:) to look like this:

@IBAction func touchIDLoginAction() {
  // 1
  touchMe.authenticateUser() { [weak self] message in
    // 2
    if let message = message {
      // if the completion is not nil show an alert
      let alertView = UIAlertController(title: "Error",
                                        message: message,
                                        preferredStyle: .alert)
      let okAction = UIAlertAction(title: "Darn!", style: .default)
      alertView.addAction(okAction)
      self?.present(alertView, animated: true)
    } else {
      // 3
      self?.performSegue(withIdentifier: "dismissLogin", sender: self)
    }
  }
}

Here’s what you’re doing in this code snippet:

  1. You’ve updated the trailing closure to accept an optional message. If biometric ID works, there is no message.
  2. You use if let to unwrap the message and display it with an alert.
  3. No change here, but if you have no message, you can dismiss the Login view.

Build and run on a physical device and test logging in with Touch ID.

Since LAContext handles most of the heavy lifting, it turned out to be relatively straight forward to implement biometric ID. As a bonus, you were able to have Keychain and biometric ID authentication in the same app, to handle the event that your user doesn’t have a Touch ID-enabled device.

Note: If you want test the errors in Touch ID, you can try to login with an incorrect finger. Doing so five times will disable Touch ID and require password authentication. This prevents strangers from trying to break into other applications on your device. You can re-enable it by going to Settings -> Touch ID & Passcode.