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:
- Add
NSFaceIDUsageDescription
to your app’sInfo.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. - Create a
LAContext
object. See the note below about the expected lifetime of this object. - Check what functionality is available on the device. You do this by calling
canEvaluatePolicy(_:error:)
on theLAContext
and passing the desiredLAPolicy
. - Once you have called
canEvaluatePolicy(_:error:)
,biometryType
is populated on theLAContext
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). - To authenticate, call
evaluatePolicy(_:localizedReason:reply:)
. The completion handler will passtrue
orfalse
, 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:
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:
On devices with Face ID, the localized reason won’t be shown until after a second failed authentication attempt.
The localizedReason
can also be set on the LAContext
, but:
- The argument in
evaluatePolicy
isn’t optional. - Passing an empty
String
triggers an assertion. - The
String
you pass here overrides whatever is set onLAContext
.
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
.
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:
LAContext.localizedCancelTitle #
You can also customize the title of the cancel button. This is done by setting localizedCancelTitle
.
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
).