Search code examples
iosxamarin.iosbiometricstouch-id

iOS Biometric authentication: Try Password work only after the second biometric fail attempt


I'm trying out the biometric authentication on iOS for the first time.

My touch id authentication code is working perfectly. But if touch id fails, I want to authenticate using Device PIN. But this is working only after the second touch id attempt fail. First time, when it fails, an alert shows with 'Try Password' button. But when touching it, instead of going to the screen to enter device pin, it shows the enter touch id alert again.

Now if the touch id fails again and if I touch the Enter password button. It's going to the screen to enter Device PIN.

But why is it not working the first time? From Apple docs:

The fallback button is initially hidden. For Face ID, after the first unsuccessful authentication attempt, the user will be prompted to try Face ID again or cancel. The fallback button is displayed after the second unsuccessful Face ID attempt. For Touch ID, the fallback button is displayed after the first unsuccessful Touch ID attempt.

I see it working with apps like google pay. What am I doing wrong here.

Here's my code.

public partial class AuthenticationViewController : UIViewController
{

    private LAContext context;

    public AuthenticationViewController(IntPtr handle) : base(handle)
    {
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        if (UserDefaultsManager.RememberMe)
            TryAuthenticate();
        else
            AppDelegate.Instance.GotoLoginController();
    }

    private void TryAuthenticate()
    {
        context = new LAContext();
        NSError error = null;

        if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0) &&
            context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics, out error)) {
            // Biometry is available on the device
            context.EvaluatePolicy(LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
                "Unlock SmartFHR", HandleLAContextReplyHandler);
        } else {
            // Biometry is not available on the device
            if (error != null) {
                HandleLAContextReplyHandler(false, error);
            } else {
                TryDevicePinAuthentication(error);
            }
        }
    }

    private void TryDevicePinAuthentication(NSError error)
    {
        if (context.CanEvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, out error)) {
            context.EvaluatePolicy(LAPolicy.DeviceOwnerAuthentication,
                "Unlock SmartFHR", HandleLAContextReplyHandler);
        }
    }

    private void HandleLAContextReplyHandler(bool success, NSError error)
    {
        DispatchQueue.MainQueue.DispatchAsync(() => {
            if (success) {
                ContinueAfterAuthSuccess();
                return;
            }
            switch (error.Code) {
                case (long)LAStatus.UserCancel:
                    AppDelegate.Instance.GotoLoginController(true);
                    break;
                case (long)LAStatus.UserFallback:
                case (long)LAStatus.BiometryNotEnrolled:
                case (long)LAStatus.BiometryNotAvailable:
                    TryDevicePinAuthentication(error);
                    break;
            }
        });
    }

    private void ContinueAfterAuthSuccess()
    {
        if (Storyboard.InstantiateViewController("SplashController") is SplashController vc)
            AppDelegate.Instance.Window.RootViewController = vc;
    }

}

When first touch id attempt fails and I touch the Try Password button, I see that it calls the HandleLAContextReplyHandler with error code LAStatus.UserFallback.


Solution

  • The documentation for LAPolicyDeviceOwnerAuthentication says:

    If Touch ID or Face ID is available, enrolled, and not disabled, the user is asked for that first.

    So when you used TryDevicePinAuthentication to display an authentication window, it will still show a biometric window first.

    If you want the user to enter the passcode to go through the authentication, I think DeviceOwnerAuthentication is enough.

    private void TryAuthenticate()
    {
        context = new LAContext();
    
        // if Biometry is available on the device, it will show it first
        context.EvaluatePolicy(LAPolicy.DeviceOwnerAuthentication, "Unlock SmartFHR", HandleLAContextReplyHandler);
    }
    

    In this way, you only need to handle the situation that the user cancels this authentication. Because it will automatically popup a passcode entering window when the user clicks the fallback button:

    private void HandleLAContextReplyHandler(bool success, NSError error)
    {
        DispatchQueue.MainQueue.DispatchAsync(() => {
            if (success)
            {
                ContinueAfterAuthSuccess();
                return;
            }
            switch (error.Code)
            {
                case (long)LAStatus.UserCancel:
                    AppDelegate.Instance.GotoLoginController(true);
                    break;
                default:
                    break;
            }
        });
    }