Using Biometrics on iOS

I recently had to dig into biometrics on iOS and thought I would write some things down, mainly for the benefit of Future Me.

Process #

Apple’s documentation has a good walkthrough of how to use biometrics to authenticate a user in your app. The process boils down to:

  1. Add NSFaceIDUsageDescription to your app’s Info.plist. This string is shown the first time your app attempts to use Face ID. If you don’t include it, your app will crash at this point.
  2. Create a LAContext object. See the note below about the expected lifetime of this object.
  3. Check what functionality is available on the device. You do this by calling canEvaluatePolicy(_:error:) on the LAContext and passing the desired LAPolicy.
  4. Once you have called canEvaluatePolicy(_:error:), biometryType is populated on the LAContext which indicates what biometrics are supported on the device. You can use this to update your UI (e.g. show a Touch ID or Face ID icon as appropriate).
  5. To authenticate, call evaluatePolicy(_:localizedReason:reply:). The completion handler will pass true or false, depending on if the attempt succeeded or failed, respectively. If it failed, the completion handler will also be passed an error.

LAPolicy #

There are various policies you can leverage, each with a different level of security. With the deviceOwnerAuthentication policy, the user can authenticate with Face ID or Touch ID, a nearby paired Apple Watch, or the device passcode. For cases where you want to tighten up security, you can require that that only biometry is used (via deviceOwnerAuthenticationWithBiometrics).

LAContext #

Avoiding Reprompts #

You can set touchIDAuthenticationAllowableReuseDuration to avoid prompting the user for authentication again if she recently unlocked her device. The default is 0, so the user will be prompted each time.

Enrollment Changes #

After a successful call to canEvaluatePolicy(_:error:) with a biometric policy (or a successful biometric authentication is performed after calling evaluatePolicy), evaluatedPolicyDomainState is populated. This property is an opaque data blob that is changed whenever the authorization database has been updated (e.g. a new finger is enrolled in Touch ID or a finger was removed from the database).

Copy #

There are a few strings you can set when interacting with the Local Authentication framework. Here is a quick overview of where they are set and where they are used.

NSFaceIDUsageDescription #

This is shown the first time your app attempts to use Face ID (i.e. the first time your app calls evaluatePolicy(_:localizedReason:reply:)). The NSFaceIDUsageDescription is shown in the system alert:

Alert to authorize Face ID usage

localizedReason #

When calling evaluatePolicy, one of the arguments you pass is the localizedReason. This explains why your app is requesting authentication. Apple’s recommendation is to:

[…] provide a clear reason for the authentication request, and describe the resulting action. Make the message short and clear, and provide it in the user’s language. Don’t include the app name, which already appears in the authentication dialog […].

On devices with Touch ID, the localized reason will be shown in the alert:

Localized reason shown with Touch ID

On devices with Face ID, the localized reason won’t be shown until after a second failed authentication attempt.

First failure with Face ID

The localizedReason can also be set on the LAContext, but:

  1. The argument in evaluatePolicy isn’t optional.
  2. Passing an empty String triggers an assertion.
  3. The String you pass here overrides whatever is set on LAContext.

Consequently, it doesn’t make sense to set LAContext.localizedReason — it will never be used.

LAContext.localizedFallbackTitle #

After a failed biometric authorization attempt, the system will show a fallback button in the authorization UI. You can set the title of this button via LAContext.localizedFallbackTitle.

localizedFallbackTitle with Touch ID
localizedFallbackTitle with Face ID

If this button is tapped, false will be passed back in the success parameter of the completion handler, and you will get userFallback as the error code. You can use that to navigate to another authentication mechanism (e.g. prompting for a PIN managed by your app).

To remove this fallback option, provide an empty string:

Empty localizedFallbackTitle removes the button

LAContext.localizedCancelTitle #

You can also customize the title of the cancel button. This is done by setting localizedCancelTitle.

localizedCancelTitle with Touch ID
localizedCancelTitle with Face ID

Gotchas #

Face ID Access #

There is no way to query the system about whether your app has already requested access to Face ID. If this is something your app needs, you will need to track it yourself.

LAContext Lifetime #

In the “Authenticator” sample code (you can download it from the “Logging a User into Your App with Face ID or Touch ID” page linked at the top) there is a seemingly innocuous comment in ViewController.swift:

// Get a fresh context for each login. If you use the same context
// on multiple attempts (by commenting out the next line), then a
// previously successful authentication causes the next policy
// evaluation to succeed without testing biometry again.
// That's usually not what you want.
context = LAContext()

The implication here is that if you reuse an instance of LAContext on which you previously called evaluatePolicy and got a success, biometrics will not be checked on the subsequent attempts; it will automatically succeed. This isn’t mentioned in the docs (I’ve filed feedback with Apple about this — FB9984036).

What this means for you, as a developer, is that your LAContext objects should probably only live for the duration of a single authentication request (assuming you want to reauthenticate each time). If you’re creating some infrastructure for managing biometrics, for example, and are holding onto an LAContext instance, it needs to be mutable so you can replace it on subsequent attempts (i.e. var context: LAContext instead of let context: LAContext).

Resources #

 
6
Kudos
 
6
Kudos

Now read this

Deciphering Xcode’s index

At work we’re having to wait an inordinate amount of time for Xcode to finish indexing our rather large Swift project. I’ve consequently spent a lot of time over the past few weeks digging into the internals of indexing. This is more or... Continue →