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.
Apple’s documentation has a good walkthrough of how to use biometrics to authenticate a user in your app. The process boils down to:
NSFaceIDUsageDescriptionto 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.
- Create a
LAContextobject. See the note below about the expected lifetime of this object.
- Check what functionality is available on the device. You do this by calling
LAContextand passing the desired
- Once you have called
biometryTypeis populated on the
LAContextwhich 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 pass
false, depending on if the attempt succeeded or failed, respectively. If it failed, the completion handler will also be passed an error.
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
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
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).
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.
This is shown the first time your app attempts to use Face ID (i.e. the first time your app calls
NSFaceIDUsageDescription is shown in the system alert:
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.
localizedReason can also be set on the
- The argument in
- Passing an empty
Stringtriggers an assertion.
Stringyou pass here overrides whatever is set on
Consequently, it doesn’t make sense to set
LAContext.localizedReason — it will never be used.
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
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:
You can also customize the title of the cancel button. This is done by setting
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
// 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).