Introduction to Biometric Authentication on iOS

Biometric authentication has become a cornerstone of modern mobile security, offering a balance between convenience and protection. For iOS developers, the LocalAuthentication framework provides a straightforward yet powerful API to integrate Touch ID, Face ID, and device passcode verification into applications. This article walks through the complete process of implementing biometric authentication using LocalAuthentication, from initial setup to production-ready best practices, ensuring your app meets the highest standards of user privacy and security.

By the end of this guide, you will understand how to check biometric availability, prompt users for authentication, handle errors gracefully, and provide fallback mechanisms—all while following Apple’s guidelines for user privacy and data protection.

Understanding the LocalAuthentication Framework

The LocalAuthentication framework, introduced in iOS 8, provides a unified interface for evaluating user identity through biometrics or a device passcode. It abstracts the underlying hardware differences between Touch ID and Face ID, allowing you to write authentication logic that works seamlessly across all supported iOS devices.

Key components of the framework include:

  • LAContext: The central object that manages authentication policies, localised reason strings, and fallback behaviour.
  • LAPolicy: Predefined policies that determine the authentication method. The two most common are .deviceOwnerAuthenticationWithBiometrics (biometrics only) and .deviceOwnerAuthentication (biometrics with passcode fallback).
  • LAError: Error codes that inform your app why authentication failed—biometry not available, user cancelled, passcode not set, and others.

While the framework is simple to use, proper implementation requires careful attention to user experience, error handling, and security. Let’s explore the policy options in more detail.

Authentication Policies Explained

  • .deviceOwnerAuthenticationWithBiometrics – If biometry (Touch ID or Face ID) is enrolled and available, this policy prompts the user for biometric verification only. If biometry is not available, the policy evaluation fails without offering a passcode fallback. This is suitable for low-risk operations where you want a frictionless experience and can handle the lack of biometry gracefully.
  • .deviceOwnerAuthentication – This policy first attempts biometric verification. If biometry is unavailable, the user fails enrolment, or the user cancels the biometric prompt, your app can fall back to the device passcode. This is the recommended policy for most authentication scenarios because it ensures that even when biometry is not an option, the user can still authenticate using their passcode.

It is important to note that the passcode fallback only appears after the user cancels the biometric prompt or if biometry fails and the user taps “Enter Passcode.” You must handle these events properly to avoid confusing the user.

Step-by-Step Implementation

Implementing biometric authentication involves five clear steps: importing the framework, creating an LAContext instance, checking whether the desired policy can be evaluated, performing the evaluation, and handling the result. Below is a detailed breakdown with code examples.

1. Import the Framework

Start by importing LocalAuthentication in any Swift file where you plan to use authentication. This import gives you access to LAContext, LAPolicy, and all error types.

import LocalAuthentication

2. Create an LAContext

LAContext is the object that queries the system state and performs the authentication evaluation. You can optionally configure properties such as the localizedReason (the message shown to the user) and the localizedFallbackTitle (the text for the fallback button). For example, you might change the fallback title to “Use Passcode” instead of the default “Enter Password.”

let context = LAContext()
context.localizedFallbackTitle = "Use Passcode"

3. Check Biometric Availability

Before prompting the user, you should check whether the chosen policy can be evaluated. This check is essential to avoid an abrupt failure that could confuse the user. Use the canEvaluatePolicy(_:error:) method, passing the policy you intend to use. The method returns a Boolean value. If it returns false, the error parameter (an NSError pointer) will contain details about why evaluation is not possible.

var error: NSError?
let canEvaluate = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)

if !canEvaluate {
    // Handle the error – see section on error handling below
    print("Authentication not available: \(error?.localizedDescription ?? "Unknown error")")
    return
}

Common reasons for failure include: the device does not support biometry, no fingerprints or face is enrolled, the device passcode is not set, or the app does not have an entitlement for Face ID (see best practices).

4. Evaluate the Policy

Once you have confirmed that the policy can be evaluated, call evaluatePolicy(_:localizedReason:reply:). The localizedReason parameter is a string that explains why your app needs authentication. This string must be clear and user-friendly because it is displayed in the system prompt. For Face ID, the reason is always shown; for Touch ID, it may be shown depending on the device configuration.

context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Authenticate to access your secure data") { success, evaluateError in
    DispatchQueue.main.async {
        if success {
            // Authentication successful
            print("User authenticated successfully")
        } else {
            // Authentication failed or was cancelled
            print("Authentication failed: \(evaluateError?.localizedDescription ?? "Unknown error")")
        }
    }
}

Notice that the completion handler is not guaranteed to run on the main thread. Always dispatch UI updates and subsequent logic to the main queue, as shown above.

5. Handle the Result

When the authentication succeeds, you can confidently allow access to protected content or actions. When it fails, you need to determine the cause by inspecting the evaluateError. The error will be of type LAError, which is an enum conforming to Error. Common cases include:

  • .userCancel: The user tapped “Cancel” on the biometric prompt. You might simply dismiss the login flow or retry.
  • .userFallback: The user tapped the fallback button (e.g., “Enter Password”). This triggers the passcode entry if using .deviceOwnerAuthentication, but if you only used .deviceOwnerAuthenticationWithBiometrics, no fallback occurs. In that case, you should present your own passcode entry screen.
  • .biometryNotAvailable: Biometry is not available on this device (e.g., missing hardware). You should switch to passcode-only authentication.
  • .biometryNotEnrolled: No biometric data is enrolled. Prompt the user to set up Touch ID or Face ID in Settings.
  • .biometryLockout: Too many failed attempts; the system has locked biometry. You need to fall back to the passcode. The system passcode will reset the lockout.
  • .passcodeNotSet: The device has no passcode configured. The user must set a passcode for biometry to work. Display an alert directing them to Settings.

Handling each of these cases gracefully is essential for a smooth user experience. For a production app, consider centralising your authentication logic in a manager class and providing clear feedback to the user.

Complete Example with Error Handling

Below is a more complete example that demonstrates a real-world authentication flow. It checks availability, handles errors with appropriate user alerts, and provides a fallback to a custom passcode entry if the user cancels biometry.

import LocalAuthentication
import UIKit

class BiometricAuthManager {
    static let shared = BiometricAuthManager()
    
    private let context = LAContext()
    
    func authenticate(completion: @escaping (Bool, String?) -> Void) {
        var error: NSError?
        context.localizedFallbackTitle = "Use Passcode"
        
        guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
            // Determine the reason why authentication is unavailable
            let message = errorMessage(for: error)
            completion(false, message)
            return
        }
        
        // Possibly show a loading state
        context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: "Authenticate to access your account") { success, evaluateError in
            DispatchQueue.main.async {
                if success {
                    completion(true, nil)
                } else {
                    let message = self.errorMessage(for: evaluateError as? LAError)
                    completion(false, message)
                }
            }
        }
    }
    
    private func errorMessage(for error: LAError?) -> String {
        guard let error = error else {
            return "Authentication could not be completed."
        }
        switch error.code {
        case .biometryNotAvailable:
            return "Biometric authentication is not available on this device."
        case .biometryNotEnrolled:
            return "No biometric data is enrolled. Please set up Face ID or Touch ID in Settings."
        case .biometryLockout:
            return "Too many failed attempts. Please use your passcode to unlock biometrics."
        case .passcodeNotSet:
            return "A device passcode is required to use biometric authentication."
        case .userCancel:
            return "Authentication cancelled by user."
        case .userFallback:
            return "User chose to use the passcode."
        default:
            return "Authentication failed: \(error.localizedDescription)"
        }
    }
}

This class encapsulates the authentication logic and returns a clean success/failure result with a user-friendly message. Your view controllers can call BiometricAuthManager.shared.authenticate(completion:) and react accordingly.

Best Practices and Production Considerations

While the code above provides a solid foundation, several additional practices will ensure your implementation is robust, secure, and user-friendly.

Always Provide a Passcode Fallback

Even if you use the .deviceOwnerAuthenticationWithBiometrics policy, you should have your own passcode entry screen ready. Many users may not have biometrics enrolled or may prefer to use a passcode in certain situations. Apple’s Human Interface Guidelines recommend offering a clear alternative to biometry.

Use a Descriptive Localized Reason

The localizedReason string is shown in the system prompt. This string should be concise and specific to the action the user is about to authorise. For example, “Sign in to your account” is better than a vague “Authentication required.” Also localise this string for different languages.

Respect User Privacy

Never store biometric data (fingerprint templates or face maps) yourself. The system securely manages this data on the Secure Enclave. Your app only receives a Boolean success/failure result, not the actual biometric data. Do not attempt to bypass this separation.

Test on Real Devices

The simulator has limited biometric simulation capabilities. Always test Touch ID and Face ID on actual iPhones and iPads. For Face ID, you must also include the NSFaceIDUsageDescription key in your Info.plist; otherwise, the app will crash when attempting to evaluate Face ID.

Handle the App Lifecycle

If your app uses biometric authentication to secure a background state, consider re-authenticating when the app returns to the foreground. You can observe UIApplication.willEnterForegroundNotification and prompt for authentication again. However, avoid asking for authentication too frequently—a common pattern is to require re-authentication only after a timeout period.

Combine with Keychain for Stronger Security

Biometric authentication alone only verifies the user’s identity at the moment of the check. For persistent security (e.g., storing API tokens), combine biometrics with the Keychain. Use the SecAccessControl class with the .biometryCurrentSet or .userPresence flags to ensure that stored secrets can only be accessed after successful biometric or passcode authentication. The Keychain then acts as a secure vault that automatically locks when the device is locked.

Fallback After Lockout

When biometry is locked due to too many failed attempts, you must fall back to the device passcode. The system passcode entry will automatically reset the biometric lockout, so after a successful passcode entry, future biometric attempts will work again. Do not attempt to bypass this; it is a security feature to protect against brute‑force attacks.

Differences Between Touch ID and Face ID

Although the LocalAuthentication framework abstracts most differences, there are a few nuances to keep in mind:

  • Face ID requires a device with a TrueDepth camera. You should check context.biometryType after calling canEvaluatePolicy to determine which biometric type is available. This allows you to adjust your UI labels and icons accordingly.
  • Face ID has higher sensitivity to travel. The user must look directly at the device. Make sure your localizedReason explains why the app needs Face ID.
  • Alternate appearance for Face ID: iOS 15.4 and later allow users to set up an alternate appearance (e.g., with glasses or a mask). Your app does not need to do anything special; the system handles it automatically.
  • Mask support with Face ID: Recent iOS versions support unlocking with a mask using the Apple Watch. For app-level authentication, the standard Face ID prompt may still require full face recognition unless the user has opted into mask use with Apple Watch. Your app’s fallback to the passcode will cover this scenario.

Error Handling Deep Dive

We briefly covered common errors, but it is worth elaborating on how to respond to each one in a user-friendly manner.

Error CodeUser Feedback
biometryNotAvailableShow an alert: “Face ID / Touch ID is not available on this device. Please use your passcode.” Then offer your custom passcode screen.
biometryNotEnrolledPresent an alert that directs the user to Settings > Face ID & Passcode (or Touch ID & Passcode). You can open Settings directly using UIApplication.openSettingsURLString.
biometryLockoutPrompt the user to authenticate using the device passcode. The system passcode entry will reset the lockout. If you use the .deviceOwnerAuthentication policy, the system automatically handles the passcode prompt. If you used .deviceOwnerAuthenticationWithBiometrics, you must fall back to your own passcode entry or invoke the .deviceOwnerAuthentication policy again to trigger the system passcode UI.
passcodeNotSetAlert the user that a device passcode is required. Direct them to Settings to set one. You cannot continue until the passcode is configured.
userCancelSimply dismiss or return to the previous screen. Do not show an error; the user intentionally cancelled.
userFallbackThe user chose the fallback option. Present your own passcode entry screen (or rely on the system passcode if you used the combined policy).

Performance and Threading

The evaluatePolicy method is asynchronous and does not block the main thread. However, the completion handler can be called on a background thread. Always dispatch UI updates to the main queue. Additionally, avoid creating a new LAContext for every authentication attempt; reuse an instance if possible, but be aware that the context can become invalid after a biometric lockout or device restart. As a best practice, create a new context for each session to ensure you have the latest state.

Testing Biometric Authentication

You can simulate biometric authentication in the iOS Simulator using the Hardware menu. For Touch ID, you can choose “Touch ID” and then “Matching Touch” or “Non-matching Touch.” For Face ID, the simulator allows you to enrol a face and then perform matching or non-matching attempts. However, some scenarios (like lockout) are not fully simulated. Therefore, real device testing is indispensable.

Additionally, you can use Xcode’s test plans to write unit tests around your authentication manager by mocking the LAContext class—provided you design your code with dependency injection. This allows you to test error handling logic without relying on actual hardware.

External Resources

For further reading and official documentation, refer to the following:

Conclusion

Implementing biometric authentication with the LocalAuthentication framework is a straightforward process that significantly enhances your iOS app’s security posture while maintaining a fluid user experience. By checking availability, handling errors gracefully, and providing reliable fallback options, you can build an authentication system that respects user privacy and meets Apple’s stringent guidelines.

Remember that biometric authentication is just one piece of a comprehensive security strategy. Combine it with secure storage via the Keychain, network security, and proper session management to offer your users the highest level of protection. With the code and best practices shared in this article, you are well equipped to integrate Touch ID and Face ID into your next iOS project.