Apple’s Face ID and Touch ID mechanisms have revolutionized mobile security by providing frictionless biometric authentication. For iOS developers, integrating these features is achieved through the LocalAuthentication framework. However, a critical design flaw in many iOS applications is relying solely on the framework’s default behavior. If biometric authentication fails (due to a dirty sensor, wearing a mask, or hardware damage), the system automatically prompts the user to enter their iOS device passcode as a fallback. For highly sensitive enterprise or financial applications, allowing the device passcode to unlock secure in-app content is often a severe compliance violation, as anyone who knows the user’s phone PIN could bypass the application’s security entirely. To enforce true Zero Trust, developers must implement custom fallback mechanisms that bypass the device passcode entirely.
The Default LocalAuthentication Context
When an iOS developer implements biometric authentication, they typically instantiate an LAContext object and invoke the evaluatePolicy method. The most common policy utilized is deviceOwnerAuthentication.
import LocalAuthentication
let context = LAContext()
var error: NSError?
// Evaluate if the device can perform authentication
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
let reason = "Authenticate to access secure financial records."
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, authenticationError in
DispatchQueue.main.async {
if success {
// Unlock application
} else {
// Handle failure
}
}
}
}
While this code works seamlessly, the .deviceOwnerAuthentication policy instructs the operating system to try Biometrics first, and if they fail or are unavailable, automatically present the standard iOS PIN/Passcode screen. If the user enters the PIN correctly, the application considers the authentication successful.
Enforcing Biometrics Only
To prevent the operating system from falling back to the device passcode, you must change the authentication policy to deviceOwnerAuthenticationWithBiometrics.
Under this policy, the LAContext will only evaluate Face ID or Touch ID. If the biometric scan fails consecutively, or if the user taps the “Cancel” button on the biometric prompt, the evaluatePolicy closure will immediately return an error, and the iOS passcode screen will not be displayed.
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authError in
DispatchQueue.main.async {
if success {
self.grantAccess()
} else {
guard let laError = authError as? LAError else { return }
self.handleBiometricFailure(error: laError)
}
}
}
}
Building a Custom Fallback Mechanism
When you enforce deviceOwnerAuthenticationWithBiometrics, the user is left stranded if their Face ID sensor breaks. Therefore, you must construct a robust custom fallback mechanism entirely within your application’s architecture.
The fallback should prompt the user for an application-specific credential—such as a custom in-app PIN, a master password, or a Time-based One-Time Password (TOTP) from an authenticator app—rather than the operating system passcode.
To trigger this custom UI elegantly, you can manipulate the native LAContext prompt by customizing the fallback button text. By default, the button says “Enter Password” (which usually triggers the OS passcode). You can change this to “Use Custom PIN” or remove the fallback button entirely.
let context = LAContext()
// Modify the fallback button text
context.localizedFallbackTitle = "Use App PIN"
// Or remove the fallback button completely
// context.localizedFallbackTitle = ""
If the user taps the custom fallback button on the Face ID prompt, the evaluatePolicy closure will return a specific error code: LAError.userFallback.
func handleBiometricFailure(error: LAError) {
switch error.code {
case .userFallback:
// The user explicitly tapped "Use App PIN"
// Present your custom SwiftUIView or UIViewController for PIN entry
presentCustomPINScreen()
case .biometryLockout:
// Too many failed biometric attempts. The sensor is locked.
// The user MUST enter the device passcode on the lock screen to re-enable biometrics,
// but within the app, we force the custom fallback.
presentCustomPINScreen()
case .userCancel:
// The user cancelled the prompt
cancelAuthenticationFlow()
default:
// Handle other edge cases (e.g., biometryNotEnrolled, biometryNotAvailable)
presentCustomPINScreen()
}
}
By strictly enforcing deviceOwnerAuthenticationWithBiometrics and intercepting the LAError.userFallback state, iOS developers can construct highly secure, compliant authentication flows that never rely on the underlying device passcode for in-app data access.