aparajita / capacitor-biometric-auth

Easy access to native biometric auth APIs on iOS and Android
MIT License
144 stars 23 forks source link

Fallback to PIN on Android #21

Closed srofi closed 11 months ago

srofi commented 11 months ago

I'm trying to implement this plugin for Android, and it works great when I do have any biometric types on device. But I'd like it to fallback to the PIN input if not. Is this possible? This is are the options I'm passing to the authenticate method right now:

{
   "reason":"Please authenticate",
   "cancelTitle":"Cancel",
   "allowDeviceCredential":true,
   "iosFallbackTitle":"Use passcode",
   "androidTitle":"Biometric login",
   "androidSubtitle":"Log in using biometric authentication",
   "androidConfirmationRequired":true
}

And it's returning this error:

{
   "save":false,
   "callbackId":"51279928",
   "pluginId":"BiometricAuthNative",
   "methodName":"internalAuthenticate",
   "success":false,
   "error": {
      "message":"There is no biometric hardware on this device.",
      "code":"biometryNotAvailable"
   }
}
srofi commented 11 months ago

To clarify, I'm seeing the PIN fallback option (since allowDeviceCredential == true), but only when displaying the biometric screen (only if the device has one)

aparajita commented 11 months ago

Device credential fallback is precisely that: credential entry after biometry is presented.

What you are proposing is to present the Android lock screen directly (which might not be a PIN, but rather a pattern) if biometry is not available. I searched online, did not find a way. Then I asked ChatGPT, and it said:

No, it's not possible to programmatically present the system PIN/pattern entry on Android due to security reasons. Android does not provide APIs for developers to directly interact with these system-level authentication mechanisms.

TheIronMarx commented 10 months ago

Hello!

I've been playing with and vetting this project today and have come across the same situation.

It would be nice for BiometricAuth.authenticate() (perhaps with some combination of options) to simply challenge the user for device security, regardless of enrollment of biometrics, pin, password, or pattern. Likewise, it would be nice for BiometricAuth.checkBiometry() to resolve positively if one of the above was enrolled rather than if and only if biometrics were enrolled. At that point, it would be more DeviceSecurity than it is BiometricAuth.

I work on a native Android and iOS app in which we have this behavior. (Not open source, sorry.) It is possible to challenge a user to enter their PIN or password in the case where they don't have biometrics enrolled.

On Android SDK 30+ devices, it can be done by tweaking the allowed authenticators of the BiometricPrompt. Something like this:

BiometricPrompt.PromptInfo.Builder().apply {
            setTitle("Title")
            setSubtitle("subtitle)
            setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)
        }.build()

On devices below Android SDK 30, I think this is the default behavior when calling KeyguardManager.createConfirmDeviceCredentialIntent(). I see the library isn't using this, however, so there may be some complexity there.

I won't dig into the iOS side of things now, but it's much less of a hassle overall.

I believe this would at least solve @srofi's problem.

This is a change that I would find immensely valuable as well.

aparajita commented 10 months ago

On Android SDK 30+ devices, it can be done by tweaking the allowed authenticators of the BiometricPrompt. Something like this: setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)

Thanks for bringing this up! I do that already, but my call to checkBiometry(), was preventing the device credential from appearing if no biometry was available. If the Android docs didn't suck so much, maybe I would have figured that out earlier!

In any case, to support this behavior (and the corresponding change to checkBiometry) I agree I really should rename the plugin to DeviceSecurity. Can't do it right now, have to finish up a project first. In the meantime, if you really need the fix, you can fork the repo, delete or comment out the lines referenced above, run pnpm build, and install the plugin directly from your local fork.

srofi commented 10 months ago

Thank you both on your replies! Yes, I managed to default to PIN/Pattern by setting allowDeviceCredential to true when calling the plugin, and modifying the check on the Android implementation of the authenticate method where it rejects the call if there's no biometric hardware on the device.

TheIronMarx commented 10 months ago

@srofi Thank you for the guidance. That seems to be enough to get it working how we'd like.

@aparajita We're making good headway on making this behave nicely in the case where a device is not enrolled with biometrics. However, it makes the API and code a little dishonest since it isn't necessarily Biometry we're asking for at this point. I think it would be good form to address that before a PR.

I see two or three general approaches here:

  1. I can modify the API such that checkBiometry() becomes checkDeviceSecurity(). This is destructive maybe worthy a major version bump.
  2. I can leave the existing API functions but add new ones that encompass this new behavior. In this case, it would be a little strange to have both checkBiometry() and checkDeviceSecurity(), but not that big of a deal
  3. I suppose a third option would be to leave everything as is, but modify the behavior to function correctly without biometrics enrolled. This is low effort, but a it makes the behavior somewhat unexpected given the function names.

At this point, I'm leaning towards option 2. Since I'd like to get this merged upstream, I would appreciate your perspective on this. Alternatively, if you would prefer not to accept a PR at all, that would be good to know here.

aparajita commented 10 months ago

@TheIronMarx Just do what you need and use your own fork. To fully support non-biometric security means creating a new plugin and then deprecating the current plugin (which you can't do), so I will not merge your changes into the existing plugin. I will make the new plugin in January.

TheIronMarx commented 10 months ago

Sounds good! I appreciate your quick response.

aparajita commented 10 months ago

@TheIronMarx On further thought, I am not going to create a new plugin so that existing users won't have to change any code. authenticate() will always attempt biometry first; device security cannot be presented independently. Thus authenticate() is the right terminology.

As for determining if device security is available, I will just add a boolean flag to CheckBiometryResult. Again, this makes sense because device security cannot be used independently of attempting biometry.

aparajita commented 10 months ago

FYI v7.0.0 has just been released which adds the following features:

This version should require no code changes. It was flagged as a potentially breaking change because of the change in the way the allowDeviceCredentials flag affects behavior.

Thanks for all of the feedback!