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
Update note: This tutorial has been updated for Xcode 9.2, Swift 4, iOS 11 and the iPhone X by Tim Mitra. The original tutorial was also written by Tim Mitra.

Protecting an app with a login screen is a great way to secure user data – you can use the Keychain, which is built right in to iOS, to ensure their data stays secure. Apple also offers yet another layer of protection with Face ID and Touch ID. 

Available since the iPhone 5S, biometric data is stored in a secure enclave in the A7 and newer chips. All of this means you can comfortably hand over the responsibility of handling login information to the Keychain and either Face ID or Touch ID. 

In this tutorial you’ll start out with static authentication. Next you’ll be using the Keychain to store and verify login information. Finally, you’ll explore using Touch ID or Face ID in your app.

Note: Face ID requires you test on a physical device. Touch ID can now be emulated in Xcode 9 in the Simulator. The Keychain can also be used in the simulator. Throughout the tutorial I refer to Touch ID and it applies to Face ID in most cases. Under the hood, is the Local Authentication framework.

Getting Started

Download the starter project for this tutorial here.

This is a basic note taking app that uses Core Data to store user notes; the storyboard has a login view where users can enter a username and password, and the rest of the app’s views are already connected to each other and ready to use.

Build and run to see what your app looks like in its current state:

TouchMeIn starter

Note: You can ignore any compiler error about Note type missing. It will be autogenerated by Core Data

At this point, tapping the Login button simply dismisses the view and displays a list of notes – you can also create new notes from this screen. Tapping Logout takes you back to the login view. If the app is pushed to the background it will immediately return to the login view; this protects data from being viewed without being logged in.

Before you do anything else, you should change the Bundle Identifier, and assign an appropriate Team.

Select TouchMeIn in the Project navigator, and then select the TouchMeIn target. In the General tab change Bundle Identifier to use your own domain name, in reverse-domain-notation – for example com.raywenderich.TouchMeIn.

Then, from the Team menu, select the team associated with your developer account like so:

With all of the housekeeping done, it’s time to code! :]

Logging? No. Log In.

To get the ball rolling, you’re going to add the ability to check the user-provided credentials against hard-coded values.

Open LoginViewController.swift and add the following constants just below managedObjectContext:

let usernameKey = "Batman"
let passwordKey = "Hello Bruce!"

These are simply the hard-coded username and password you’ll check the user-provided credentials against.

Next, add the following method below loginAction(_:):

func checkLogin(username: String, password: String) -> Bool {
  return username == usernameKey && password == passwordKey
}

Here you check the user-provided credentials against the constants previously defined.

Next, replace the contents of loginAction(_:) with the following:

if checkLogin(username: usernameTextField.text!, password: passwordTextField.text!) {
  performSegue(withIdentifier: "dismissLogin", sender: self)
}

Here you call checkLogin(username:password:), which dismisses the login view only if the credentials are correct.

Build and run. Enter the username Batman and the password Hello Bruce!, and tap the Login button. The login screen should dismiss as expected.

While this simple approach to authentication seems to work, it’s not terribly secure, as credentials stored as strings can easily be compromised by curious hackers with the right tools and training. As a best practice, passwords should NEVER be stored directly in the app. To that end, you’ll employ the Keychain to store the password.

Note: Passwords in most apps are simply strings and are hidden as bullets. The best way to handle a password in your app is to SALT it and/or encrypt it with a SHA-2 encryption as soon as it is captured. Only the user should know the actual string. This is beyond the scope of this tutorial, but you should keep this in mind.

Check out Ryan Ackermann’s Basic iOS Security: Keychain and Hashing tutorial for the lowdown on how the Keychain works as well as password salting.

Rapper? No. Wrapper.

The next step is to add a Keychain wrapper to your app.

Along with the starter project, you downloaded a folder with useful resources. Locate and open the Resources folder in Finder. You’ll see the file KeychainPasswordItem.swift; this class comes from Apple’s sample code GenericKeychain.

Drag the KeychainPasswordItem.swift into the project, like so:

When prompted, make sure Copy items if needed and TouchMeIn target are both checked:

Copy files if needed

Build and run to make sure you have no errors. All good? Great — now you can leverage the Keychain from within your app.

Keychain, Meet Password. Password, Meet Keychain

To use the Keychain, you first store a username and password in it. Next, you’ll check the user-provided credentials against the Keychain to see if they match.

You’ll track whether the user has already created some credentials so you can change the text on the Login button from “Create” to “Login”. You’ll also store the username in the user defaults so you can perform this check without hitting the Keychain each time.

The Keychain requires some configuration to properly store your app’s information. You’ll provide that configuration in the form of a serviceName and an optional accessGroup. You’ll use a struct to store these values.

Open LoginViewController.swift. Add the following just below the import statements:

// Keychain Configuration
struct KeychainConfiguration {
  static let serviceName = "TouchMeIn"
  static let accessGroup: String? = nil
}

Next, add the following below managedObjectContext:

var passwordItems: [KeychainPasswordItem] = []
let createLoginButtonTag = 0
let loginButtonTag = 1

@IBOutlet weak var loginButton: UIButton!

passwordItems is an empty array of KeychainPasswordItem types you’ll pass into the keychain. You’ll use the next two constants to determine if the Login button is being used to create some credentials, or to log in; you’ll use the loginButton outlet to update the title of the button depending on its state.

Next, you’ll handle the two cases for when the button is tapped: if the user hasn’t yet created their credentials, the button text will show “Create”, otherwise the button will show “Login”.

First you’ll need a way to tell the user if the login fails. Add the following after checkLogin(username:password:):

private func showLoginFailedAlert() {
  let alertView = UIAlertController(title: "Login Problem",
                                    message: "Wrong username or password.",
                                    preferredStyle:. alert)
  let okAction = UIAlertAction(title: "Foiled Again!", style: .default)
  alertView.addAction(okAction)
  present(alertView, animated: true)
}

Now, replace loginAction(sender:) with the following:

@IBAction func loginAction(sender: UIButton) {
  // 1
  // Check that text has been entered into both the username and password fields.
  guard let newAccountName = usernameTextField.text,
    let newPassword = passwordTextField.text,
    !newAccountName.isEmpty,
    !newPassword.isEmpty else {
      showLoginFailedAlert()
      return
  }
    
  // 2
  usernameTextField.resignFirstResponder()
  passwordTextField.resignFirstResponder()
    
  // 3
  if sender.tag == createLoginButtonTag {
    // 4
    let hasLoginKey = UserDefaults.standard.bool(forKey: "hasLoginKey")
    if !hasLoginKey && usernameTextField.hasText {
      UserDefaults.standard.setValue(usernameTextField.text, forKey: "username")
    }
      
    // 5
    do {
      // This is a new account, create a new keychain item with the account name.
      let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName,
                                              account: newAccountName,
                                              accessGroup: KeychainConfiguration.accessGroup)
        
      // Save the password for the new item.
      try passwordItem.savePassword(newPassword)
    } catch {
      fatalError("Error updating keychain - \(error)")
    }
      
    // 6
    UserDefaults.standard.set(true, forKey: "hasLoginKey")
    loginButton.tag = loginButtonTag
    performSegue(withIdentifier: "dismissLogin", sender: self)
  } else if sender.tag == loginButtonTag {
     // 7
    if checkLogin(username: newAccountName, password: newPassword) {
      performSegue(withIdentifier: "dismissLogin", sender: self)
    } else {
      // 8
      showLoginFailedAlert()
    }
  }
}

Here’s what’s happening in the code:

  1. If either the username or password is empty, you present an alert to the user and return from the method.
  2. Dismiss the keyboard if it’s visible.
  3. If the login button’s tag is createLoginButtonTag, then proceed to create a new login.
  4. Next, you read hasLoginKey from UserDefaults which you use to indicate whether a password has been saved to the Keychain. If hasLoginKey is false and the username field has any text, then you save that text as username to UserDefaults.
  5. You create a KeychainPasswordItem with the serviceNamenewAccountName (username) and accessGroup. Using Swift’s error handling, you try to save the password. The catch is there if something goes wrong.
  6. You then set hasLoginKey in UserDefaults to true to indicate a password has been saved to the keychain. You set the login button’s tag to loginButtonTag to change the button’s text, so it will prompt the user to log in the next time they run your app, rather than prompting the user to create a login. Finally, you dismiss loginView.
  7. If the user is logging in (as indicated by loginButtonTag), you call checkLogin to verify the user-provided credentials; if they match then you dismiss the login view.
  8. If the login authentication fails, then present an alert message to the user.
Note: Why not just store the password along with the username in UserDefaults? That would be a bad idea because values stored in UserDefaults are persisted using a plist file. This is essentially an XML file that resides in the app’s Library folder, and is therefore readable by anyone with physical access to the device. The Keychain, on the other hand, uses the Triple Digital Encryption Standard (3DES) to encrypt its data. Even if somebody gets the data, they won’t be able to read it.

Next, replace checkLogin(username:password:) with the following updated implementation:

func checkLogin(username: String, password: String) -> Bool {
  guard username == UserDefaults.standard.value(forKey: "username") as? String else {
    return false
  }
    
  do {
    let passwordItem = KeychainPasswordItem(service: KeychainConfiguration.serviceName,
                                            account: username,
                                            accessGroup: KeychainConfiguration.accessGroup)
    let keychainPassword = try passwordItem.readPassword()
    return password == keychainPassword
  } catch {
    fatalError("Error reading password from keychain - \(error)")
  }
}

Here you check that the username entered matches the one stored in UserDefaults and that the password matches the one stored in the Keychain.

Next, delete the following lines:

let usernameKey = "Batman"
let passwordKey = "Hello Bruce!"

Now it’s time to set the button title and tags appropriately depending on the state of hasLoginKey.

Add the following code to viewDidLoad(), just below the call to super:

// 1
let hasLogin = UserDefaults.standard.bool(forKey: "hasLoginKey")
    
// 2
if hasLogin {
  loginButton.setTitle("Login", for: .normal)
  loginButton.tag = loginButtonTag
  createInfoLabel.isHidden = true
} else {
  loginButton.setTitle("Create", for: .normal)
  loginButton.tag = createLoginButtonTag
  createInfoLabel.isHidden = false
}
    
// 3
if let storedUsername = UserDefaults.standard.value(forKey: "username") as? String {
  usernameTextField.text = storedUsername
}

Taking each numbered comment in turn:

  1. You first check hasLoginKey to see if you’ve already stored a login for this user.
  2. If so, change the button’s title to Login, update its tag to loginButtonTag, and hide createInfoLabel, which contains the informative text “Start by creating a username and password“. In case you don’t have a stored login for this user, you set the button label to Create and display createInfoLabel to the user.
  3. Finally, you set the username field to what is saved in UserDefaults to make logging in a little more convenient for the user.

Finally, you need to connect your outlet to the Login button. Open Main.storyboard and select the Login View Controller Scene. Ctrl-drag from the Login View Controller to the Login button, as shown below:

From the resulting popup, choose loginButton:

Build and run. Enter a username and password of your own choosing, then tap Create.

Note: If you forgot to connect the loginButton IBOutlet then you might see the error Fatal error: unexpectedly found nil while unwrapping an Optional value. If you do, connect the outlet as described in the relevant step above.

Now tap Logout and attempt to login with the same username and password – you should see the list of notes appear.

Tap Logout and try to log in again; this time, use a different password and then tap Login. You should see the following alert:

wrong password

Congratulations – you’ve now added authentication use the Keychain. Next up, Touch ID.