Home / Blog / Face ID and Touch ID: using LocalAuthentication the right way

Face ID and Touch ID: using LocalAuthentication the right way

Biometric auth is a premium feature on iOS. LocalAuthentication has a clean API, but using it wrong puts user security at risk. Practical patterns from five shipped apps.

Biometric authentication has become table stakes in iOS apps. Locking the app with Face ID or Touch ID, confirming sensitive operations, using biometric-backed keychain. The framework: LocalAuthentication.

I’ve integrated biometrics in five shipped apps. A banking app, a finance tracker, a medical records app, and two privacy-sensitive productivity apps. Each one needed a slightly different pattern.

The basic API

import LocalAuthentication

let context = LAContext()
var error: NSError?

guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
    print("Biometric not available: (error?.localizedDescription ?? "")")
    return
}

context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "To access the app") { success, error in
    DispatchQueue.main.async {
        if success {
            // Auth success
        } else {
            // Auth failed
        }
    }
}

Four lines. But the correct usage is all in the details.

Choosing the policy

Two main policies:

.deviceOwnerAuthenticationWithBiometrics: Face ID or Touch ID only. If biometric fails, the user has nothing else to try.

.deviceOwnerAuthentication: biometric with fallback to passcode. If biometric fails, the device passcode prompt appears.

I usually prefer .deviceOwnerAuthentication. When Face ID refuses in certain conditions (mask, dim room, sunglasses), the user can continue with passcode. Less friction.

Exception: for certain financial operation confirmations, “biometric only” discipline can fit. Passcode carries a somebody-else-knows risk.

localizedReason: what you’re telling the user

The text that shows in the Face ID prompt. It needs to be specific and action-oriented.

Bad: “Authentication”
Good: “Use Face ID to access your financial data”

The user should be able to answer “why is it asking for biometric?” in one second. Vague copy breeds mistrust.

Localized version:

context.evaluatePolicy(
    .deviceOwnerAuthenticationWithBiometrics,
    localizedReason: NSLocalizedString(
        "biometric.reason.vault",
        value: "Unlock the vault",
        comment: ""
    )
) { ... }

Biometry type detection

Does the device have Face ID or Touch ID? You need to know to show the right copy:

let context = LAContext()
var error: NSError?
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)

switch context.biometryType {
case .faceID:
    buttonTitle = "Sign in with Face ID"
case .touchID:
    buttonTitle = "Sign in with Touch ID"
case .opticID:
    buttonTitle = "Sign in with Optic ID" // Vision Pro
case .none:
    buttonTitle = "Sign in with passcode"
@unknown default:
    buttonTitle = "Secure sign in"
}

Call canEvaluatePolicy before checking biometryType, otherwise the type comes back as .none.

Icon choice: use SF Symbols faceid or touchid. Seeing the icon tells the user what’s about to happen.

Info.plist permission

Since iOS 11.3, Face ID requires a permission string. In Info.plist:

<key>NSFaceIDUsageDescription</key>
<string>Sign in to your app quickly and securely with Face ID.</string>

Without that string, the Face ID prompt never shows, and the app crashes in review. Touch ID doesn’t have a separate permission.

LAError handling

Error types that can come back during evaluation:

context.evaluatePolicy(...) { success, error in
    if let error = error as? LAError {
        switch error.code {
        case .userCancel:
            // User hit Cancel, move on silently
        case .userFallback:
            // User tapped "Use Passcode", route to the passcode flow
        case .biometryNotAvailable:
            // No biometric on device or it's off
            showError("Face ID is not available")
        case .biometryNotEnrolled:
            // Biometric exists but nothing enrolled
            showError("Face ID isn't set up. Add it in Settings.")
        case .biometryLockout:
            // 5 failed attempts, passcode required
            showError("Face ID is temporarily locked. Passcode required.")
        case .passcodeNotSet:
            // No device passcode set
            showError("No device passcode is set.")
        case .systemCancel:
            // System canceled the call (incoming call, etc.)
        case .invalidContext:
            // Context invalid
        default:
            showError("Could not authenticate.")
        }
    }
}

A different user message for each error type. A generic “auth failed” frustrates the user.

Biometric-backed Keychain

The real power of biometric auth is protecting the Keychain with it. The user’s password, token, or secret key is only readable after biometric auth.

let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
    .biometryCurrentSet,
    nil
)

let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrService as String: "com.example.vault",
    kSecAttrAccount as String: "user_token",
    kSecValueData as String: token.data(using: .utf8)!,
    kSecAttrAccessControl as String: access
]
SecItemAdd(query as CFDictionary, nil)

The .biometryCurrentSet flag: “only the currently enrolled biometric set can read this”. If the user adds a new finger or face in Settings, the old Keychain item is invalidated.

The .biometryAny alternative: any biometric match (current or added later). For security-sensitive apps, .biometryCurrentSet is stricter.

Retry policy

What do you do when biometric fails? Don’t auto-retry. Let the user tap a button to try again.

Biometric auth failed
 -> Show "Try again" button
 -> User tap triggers evaluatePolicy again

Automatic retry spams the Face ID dialog and feels aggressive.

When the app goes to background

You often want to re-lock with biometrics when the app backgrounds. Common pattern:

func applicationWillResignActive(_ application: UIApplication) {
    lockStartTime = Date()
}

func applicationDidBecomeActive(_ application: UIApplication) {
    if let startTime = lockStartTime,
       Date().timeIntervalSince(startTime) > 60 {
        // Backgrounded for more than 60 seconds, lock required
        showBiometricLock()
    }
    lockStartTime = nil
}

If the user hops to another app and comes back 10 seconds later, re-unlocking is unnecessary. 60 seconds is a good threshold. 30 seconds for sensitive apps, lock on every background for ultra-sensitive ones.

Two-factor: biometric plus something

Biometric alone isn’t enough in every case. For high-value operations, adding a second factor is the security best practice.

Pattern:

  • Low risk: biometric is enough
  • Medium risk: biometric plus PIN
  • High risk: biometric plus SMS OTP or time-based OTP

Biometric isn’t immune to replay attacks (bypassing Face ID with a photo is rare but possible). Don’t trust biometric alone for high-value operations.

Testing discipline

The simulator can fake Face ID (Features -> Face ID -> Matching Face / Non-matching Face). Touch ID can be faked too.

Good for fast iteration. Final testing has to happen on real devices. Edge cases (masked Face ID, wet finger Touch ID) can’t be emulated in the simulator.

Let the user opt in

Offering biometric as opt-in is better UX than forcing it. On first launch, “Would you like to use Face ID to sign in?” and if yes, save biometric-backed to Keychain.

Users who decline stay on the normal password flow. Users who opt in take the extra friction, but it was their choice.

Performance

Face ID takes around 1 second, Touch ID around 0.5. The UI needs to be ready in that window. After a successful biometric prompt, transition instantly. On failure, fall back UI.

Triggering the biometric prompt during the splash screen means the user is already waiting, so it becomes a single wait. Showing a separate lock screen and then prompting for biometrics creates two waits stacked on each other.

Closing advice

Biometric auth is a security feature but also a UX feature. Done right, users love it. Done wrong, it adds friction.

Principles:

  • Specific reason string
  • User messages matched to error types
  • Biometric-backed Keychain for sensitive data
  • Balanced background lock timing
  • Opt-in by default
  • Combine with 2FA for high risk

Apply these six and biometric auth becomes a premium part of the app’s security and experience.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch