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.
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
How To Secure iOS User Data: The Keychain and Biometrics — Face ID or Touch ID
35 mins
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:
-
authenticateUser(completion:)
takes a completion handler in the form of a closure. - You’re using
canEvaluatePolicy()
to check whether the device is capable of biometric authentication. - 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. - 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
- Declare a string to hold the message.
- 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. - 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.
-
- 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)
}
}
}
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:
- You’ve updated the trailing closure to accept an optional message. If biometric ID works, there is no message.
- You use
if let
to unwrap the message and display it with an alert. - 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.