Search code examples
androidbiometricsandroid-biometric-prompt

Androidx.biometric with only device credentials installed does not call callback on devices with Android 9 and bellow


we implemented authentication (and signing) using the androidx support library. It works perfect on Device with Android 10 (also the DeviceCredentialUnlock) and with Biometrics (the code is little different, as we want to know if it was signed with biometrics or with device credentials).

With Android 8 and older it never calls the callback. No error message is logged, the CompletableFuture is never completed.

   CompletableFuture<String> signingResult = new CompletableFuture<>();

    final String title = fragmentActivity.getString(R.string.LockScreenTitle);

    final String description = fragmentActivity.getString(R.string.LockScreenDescription);
    
    final String data = "testData";

    Executor executor = ContextCompat.getMainExecutor(fragmentActivity);

    final BiometricPrompt.AuthenticationCallback callback = new BiometricPrompt.AuthenticationCallback() {

        @Override
        public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
            LOGGER.info("onAuthenticationError");
            Exception exception = mapAuthenticationError(errorCode, errString);
            signingResult.completeExceptionally(exception);
        }

        @Override
        public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
            super.onAuthenticationSucceeded(result);
            try {
                LOGGER.info("onAuthenticationSucceeded");
                String signedData = SignInternal(data);
                signingResult.complete(signedData);
            } catch (SecureTokenException e) {
                signingResult.completeExceptionally(e);
            }
        }

        @Override
        public void onAuthenticationFailed() {
            LOGGER.info("onAuthenticationFailed");
            signingResult.completeExceptionally(new SecureTokenException(SecureTokenException.ErrorCode.ABORTED_BY_USER));
        }
    };

    BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setDescription(description)
            .setDeviceCredentialAllowed(true)
            .build();

    final BiometricPrompt bp = new BiometricPrompt(fragmentActivity, executor, callback);


    fragmentActivity.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            bp.authenticate(promptInfo);
        }
    });

    try {
        String signed = signingResult.get();

The keys are generated with the .setUserAuthenticationValidityDurationSeconds(5); property. the sign Method uses the authenticated for signing. But the callback is never called, so we do not come to this point (at Android 9...).

Thank you for your help!


Solution

  • I've recently implemented the androidx biometric lib in my app. First of all the BiometricPrompt.PromptInfo.Builder.setDeviceCredentialAllowed() is deprecated and we should now use the BiometricPrompt.PromptInfo.Builder.setAllowedAuthenticators(int) instead. The int parameter passed to that function must be one of or a combination of the enum values in BiometricManager.Authenticators:

    • BIOMETRIC_STRONG
    • BIOMETRIC_WEAK
    • DEVICE_CREDENTIAL

    This Android Developer docs describes the different enums: https://developer.android.com/reference/androidx/biometric/BiometricManager.Authenticators

    From Android Developer docs on Biometric Authentication (I recommend reading this guide btw):

    Note: The following combinations of authenticator types aren't supported on Android 10 (API level 29) and lower: DEVICE_CREDENTIAL and BIOMETRIC_STRONG | DEVICE_CREDENTIAL. To check for the presence of a PIN, pattern, or password on Android 10 and lower, use the KeyguardManager.isDeviceSecure() method.

    Below is a small block of code that I use to setup my biometric prompt, hopefully this and the information above will guide you in the right direction.

    val bioPromptInfoBuilder = BiometricPrompt.PromptInfo.Builder()
        .setTitle(authPromptParams.promptTitle)
        .setSubtitle(authPromptParams.promptSubtitle)
    
    when {
        Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> {
            bioPromptInfoBuilder.setAllowedAuthenticators(
                BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
            )
        }
        kgm.isDeviceSecure -> {
            bioPromptInfoBuilder.setAllowedAuthenticators(
                BiometricManager.Authenticators.BIOMETRIC_WEAK or
                    BiometricManager.Authenticators.DEVICE_CREDENTIAL)
        }
        else -> return null
    }
    

    Basically Android 10 and below supports different combinations of the BiometricManager.Authenticators enums.