Introduction to Biometric Authentication on iOS

Biometric authentication – Face ID and Touch ID – has transformed how users secure and access their iOS apps. By integrating Apple’s Local Authentication framework, you offer a frictionless yet highly secure login experience. Users can authorise purchases, unlock content, or sign into accounts with nothing more than a glance or a touch. This guide covers everything you need to know to implement Face ID and Touch ID in your iOS app using Swift, from project setup to production‑ready error handling and fallback mechanisms.

Biometrics are now a cornerstone of modern mobile UX. They eliminate the need to remember complex passwords, reduce friction, and dramatically lower the risk of account takeover. According to Apple, over 90% of devices in active use support either Face ID or Touch ID. Integrating them not only pleases users but also meets the security expectations of today’s marketplace.

Prerequisites

Before you start coding, ensure you have the following:

  • macOS 11 (Big Sur) or later with Xcode 13 or higher.
  • An iPhone or iPad with a supported biometric sensor:
    • Touch ID – iPhone 5s or later, iPad Air 2 or later, iPad mini 3 or later.
    • Face ID – iPhone X or later, iPad Pro (3rd generation) or later.
  • Target iOS 13+ for full API support (the LAPolicy and error handling used here are available from iOS 8 onward, but newer features like .deviceOwnerAuthenticationWithBiometrics and better fallback flows work best on iOS 13+).
  • Basic familiarity with Swift and Xcode projects, including editing Info.plist.
  • An Apple Developer account (free accounts can run on physical devices, but you need a paid account for distribution).

Setting Up Your Xcode Project

Open your existing iOS project in Xcode, or create a new one (File → New → Project → App). We’ll add the necessary permissions and imports.

1. Add the Face ID Usage Description

Apple requires app Info.plist to contain the NSFaceIDUsageDescription key whenever your app uses Face ID. This string is displayed in the system prompt when the user is first asked to permit biometric authentication. Without it, your app will crash when you attempt to evaluate a Face ID policy.

To add the key:

  • Open Info.plist in the project navigator.
  • Click the + button next to any existing key.
  • Type or select Privacy - Face ID Usage Description from the dropdown.
  • For the value, enter a clear, brief reason. For example: "Authenticate to access your secure account details."

If your app supports only Touch ID, you can skip this key – but adding it won’t hurt and makes your app forward‑compatible with future devices.

2. Import the Local Authentication Framework

In the Swift file where you’ll call biometrics, add the following import at the top:

import LocalAuthentication

That’s all the setup needed. The framework’s LAContext class handles all the heavy lifting.

Implementing Biometric Authentication – Step by Step

We’ll build a reusable method that checks biometric availability, evaluates the policy, and returns a result with clear error handling.

1. Create an Authentication Manager

Start by creating a class or a standalone function. A dedicated manager helps keep your authentication logic testable and reusable across multiple view controllers.

import LocalAuthentication

class BiometricAuthManager {
    private let context = LAContext()
    
    /// Attempts biometric authentication and calls the completion with success or error.
    func authenticateUser(reason: String, completion: @escaping (Result<Void, BiometricError>) -> Void) {
        // Reset the context before evaluating – important for repeated use
        context.invalidate()
        
        var authError: NSError?
        let canEvaluate = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &authError)
        
        guard canEvaluate else {
            completion(.failure(mapError(authError)))
            return
        }
        
        context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, evaluateError in
            DispatchQueue.main.async {
                if success {
                    completion(.success(()))
                } else {
                    completion(.failure(self.mapError(evaluateError as NSError?)))
                }
            }
        }
    }
    
    private func mapError(_ error: NSError?) -> BiometricError {
        // We'll define BiometricError below
        // ...
    }
}

enum BiometricError: Error {
    case biometryNotAvailable
    case biometryNotEnrolled
    case biometryLockout
    case userCancel
    case userFallback
    case systemCancel
    case appCancel
    case invalidContext
    case notInteractive
    case passcodeNotSet
    case unknown(NSError)
}

Let’s break down the key parts:

  • LAContext – This object encapsulates the authentication state. We call invalidate() before each use to clear any previous evaluation and prevent reuse of cached credentials.
  • canEvaluatePolicy – Checks whether the device can perform biometric authentication. It returns true only if the device has a biometric sensor and the user has enrolled a face or fingerprint. If it returns false, the error parameter tells us why.
  • evaluatePolicy – This triggers the actual system dialog. The localizedReason (the same as your plist description for Face ID) is shown to the user. The closure returns a boolean success and an optional error.

2. Mapping Apple’s Error Codes

Apple provides error constants via LAError.Code. A robust implementation maps them to your own enum so you can provide actionable feedback to the user or developer.

private func mapError(_ error: NSError?) -> BiometricError {
    guard let error = error else { return .unknown(NSError(domain: "BiometricAuth", code: -1, userInfo: nil)) }
    let code = LAError.Code(rawValue: error.code) ?? .appCancel
    switch code {
    case .biometryNotAvailable:
        return .biometryNotAvailable
    case .biometryNotEnrolled:
        return .biometryNotEnrolled
    case .biometryLockout:
        return .biometryLockout
    case .userCancel:
        return .userCancel
    case .userFallback:
        return .userFallback
    case .systemCancel:
        return .systemCancel
    case .appCancel:
        return .appCancel
    case .invalidContext:
        return .invalidContext
    case .notInteractive:
        return .notInteractive
    case .passcodeNotSet:
        return .passcodeNotSet
    default:
        return .unknown(error)
    }
}

Common scenarios:

  • .biometryNotAvailable – No Face ID / Touch ID sensor on the device (e.g., simulator, iPod Touch).
  • .biometryNotEnrolled – The sensor exists but the user hasn’t enrolled a face or fingerprint in Settings.
  • .biometryLockout – Too many failed attempts; the sensor is disabled until the user enters their device passcode.
  • .userCancel – The user tapped “Cancel” on the system dialog.
  • .userFallback – The user tapped “Enter Passcode” (if you’ve allowed fallback).

3. Handling the Authentication Result

In your view controller, call the authentication method and react to the result.

let authManager = BiometricAuthManager()
authManager.authenticateUser(reason: "Unlock the vault to view your saved passwords.") { result in
    switch result {
    case .success:
        // Navigate to the secure screen or perform the protected action
        self.showSecureContent()
    case .failure(let error):
        self.handleBiometricError(error)
    }
}

private func handleBiometricError(_ error: BiometricError) {
    switch error {
    case .userCancel:
        // Do nothing – user cancelled intentionally
        break
    case .userFallback:
        // Show your custom passcode screen
        self.presentPasscodeEntry()
    case .biometryLockout:
        // Inform the user that they must enter the device passcode
        self.showAlert(title: "Biometric Locked", message: "Too many failed attempts. Please use your device passcode to unlock.")
    case .biometryNotEnrolled:
        self.showAlert(title: "Biometric Not Set Up", message: "Go to Settings > Face ID & Passcode (or Touch ID & Passcode) to enroll.")
    case .biometryNotAvailable:
        self.showAlert(title: "Biometric Not Available", message: "This device does not support Face ID or Touch ID.")
    default:
        self.showAlert(title: "Authentication Failed", message: "An unexpected error occurred. Please try again.")
    }
}

Including a Fallback to Device Passcode

For best UX, always allow the user to fall back to the device passcode. Use the policy .deviceOwnerAuthentication instead of .deviceOwnerAuthenticationWithBiometrics. If biometrics are available, the system will still prompt for biometrics first, but it also shows an “Enter Passcode” button.

func authenticateWithPasscodeFallback(reason: String, completion: @escaping (Result<Void, Error>) -> Void) {
    context.invalidate()
    var error: NSError?
    let canEvaluate = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error)
    guard canEvaluate else {
        // If even passcode authentication is unavailable (e.g., no passcode set on device),
        // you should guide the user to set a passcode.
        completion(.failure(error ?? BiometricError.passcodeNotSet))
        return
    }
    context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authError in
        DispatchQueue.main.async {
            if success {
                completion(.success(()))
            } else {
                completion(.failure(authError ?? BiometricError.unknown(NSError())))
            }
        }
    }
}

Note: If you use .deviceOwnerAuthentication, the system can also handle cases where biometrics are locked out by immediately prompting for the passcode. This is often the best choice for actions like app unlock or payments, as it provides a seamless path to recovery.

Testing Your Implementation

You cannot test biometric prompts on the iOS Simulator. Simulating a biometric match is possible using the simulator’s “Face ID / Touch ID” menu item (Hardware → Face ID or Hardware → Touch ID), but the actual system dialog won’t appear. For complete testing, you must run on a physical device.

Test the following scenarios:

  1. Successful authentication – present a matched face or fingerprint.
  2. Failed authentication – present a non‑matching biometric.
  3. User cancel – tap Cancel on the dialog.
  4. Lockout – fail biometrics multiple times until the system disables it.
  5. No biometrics enrolled – temporarily remove all fingerprints/faces in device Settings → Face ID & Passcode / Touch ID & Passcode.
  6. No passcode set – turn off the device passcode entirely (be careful – this makes the device less secure).
  7. Multiple rapid calls – ensure you handle context invalidation to avoid crashes.

Also test on both Face ID and Touch ID devices if you can. Face ID expects the user to look at the camera, while Touch ID requires a clean finger on the sensor. Your code is identical for both – Apple’s framework abstracts the hardware.

Best Practices and Security Considerations

  • Never cache or store biometric data. The Local Authentication framework handles everything; your app never receives the actual face map or fingerprint. Only the boolean success/failure is exposed.
  • Use biometrics for sensitive operations only. For example, unlocking an app, authorising purchases, or viewing private data. Avoid unnecessary biometric prompts for trivial actions.
  • Provide a clear and honest localizedReason. The system shows this to the user; make it descriptive enough that they understand why you’re requesting authentication. e.g., “Authenticate to view your health records.”
  • Handle .userFallback gracefully. When the user selects “Enter Passcode”, you should present your own authentication screen or use the device passcode via .deviceOwnerAuthentication.
  • Reset the context for each attempt. Call context.invalidate() before evaluating a new policy to prevent unintended reuse of previous credentials.
  • Consider combining biometrics with a server‑side token. For high‑security features (e.g., bank transfers), use biometrics to authorise an access token stored in the Keychain, rather than relying solely on local authentication.
  • Support both Face ID and Touch ID transparently. Write your code once – Apple’s framework adjusts the UI automatically (Face ID uses an oval animation on the notch, Touch ID shows a fingerprint icon).
  • Test on devices with different screen sizes and post‑Face ID updates (e.g., iPad Pro with Face ID).

Advanced Topics and Further Reading

LocalAuthentication & the Keychain

You can combine biometric authentication with the Keychain to protect secrets. Use SecAccessControlCreateWithFlags with the .biometryCurrentSet or .userPresence constraint. This ensures that the Keychain item can only be accessed after a successful biometric (or passcode) check. Example:

let access = SecAccessControlCreateWithFlags(nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    .userPresence,  // biometrics or passcode
    nil)

SwiftUI Integration

In SwiftUI, present the authentication flow from a .task modifier or a button action. Use the same LAContext API. For example:

Button("Unlock") {
    let context = LAContext()
    context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Access your journals") { success, error in
        if success { /* navigate */ }
    }
}

Supporting Older iOS Versions

If you support iOS 8–12, note that LAError.Code values like .biometryLockout were added later. Always check availability with if #available(iOS 11.0, *) before using newer codes. For iOS 11+, you can also check the context.biometryType property to determine whether the device uses Face ID or Touch ID, which can be useful for custom UI:

switch context.biometryType {
case .faceID:
    // Show Face ID icon
case .touchID:
    // Show Touch ID icon
case .none:
    // Biometrics unavailable
}

Conclusion

Integrating Face ID and Touch ID into your iOS app is straightforward with the Local Authentication framework. You now have a reusable, production‑ready authentication flow that handles availability checks, fallback to passcode, and all common error scenarios. Biometric authentication not only improves security – by reducing the risk of weak passwords – but also delights users with a fast, intuitive unlock experience. As you expand your app, remember to follow Apple’s Human Interface Guidelines for biometrics (Face ID and Touch ID HIG) and consult the Local Authentication documentation for the latest API updates. Start implementing today and give your users the secure, effortless authentication they expect.