Search code examples
androidkotlinandroid-biometric-promptandroid-biometric

Android biometric authentication invalid variables in AuthenticationCallback when using device credential


I am using androidx.biometric:biometric:1.0.1 everything works fine but when I have a device without a biometric sensor (or when the user didn't set his fingerprint or etc) and I try to use DeviceCredentials after doing authentication my function input data is not valid.

class MainActivity : AppCompatActivity() {

    private val TAG = MainActivity::class.java.name

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        findViewById<View>(R.id.first).setOnClickListener {
            authenticate(MyData(1, "first"))
        }

        findViewById<View>(R.id.second).setOnClickListener {
            authenticate(MyData(2, "second"))
        }
    }

    private fun authenticate(data: MyData) {
        Log.e(TAG, "starting auth with $data")
        val biometricPrompt = BiometricPrompt(
            this,
            ContextCompat.getMainExecutor(this),
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    Log.e(TAG, "auth done : $data")
                }
            })

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setDeviceCredentialAllowed(true)
            .setTitle("title")
            .build()
        biometricPrompt.authenticate(promptInfo)
    }
}

data class MyData(
    val id: Int,
    val text: String
)

First I click on my first button, authenticate, then I click my second button and authenticate, then android logcat is like this:

E/com.test.biometrictest.MainActivity: starting auth with MyData(id=1, text=first)
E/com.test.biometrictest.MainActivity: auth done : MyData(id=1, text=first)
E/com.test.biometrictest.MainActivity: starting auth with MyData(id=2, text=second)
E/com.test.biometrictest.MainActivity: auth done : MyData(id=1, text=first)

as you see in last line MyData id and text is invalid! autneticate function input(data) is not the same when onAuthenticationSucceeded is called!

(if you try to test it be sure to use DeviceCredentials not biometrics, I mean pattern or password, unset your fingerprint) Why data is not valid in callBack?

it works ok on android 10 or with fingerprint

I don`t want to use onSaveInstanceState.


Solution

  • When you create a new instance of BiometricPrompt class, it adds a LifecycleObserver to the activity and as I figured out it never removes it. So when you have multiple instances of BiometricPrompt in an activity, there are multiple LifecycleObserver at the same time that cause this issue.

    For devices prior to Android Q, there is a transparent activity named DeviceCredentialHandlerActivity and a bridge class named DeviceCredentialHandlerBridge which support device credential authentication. BiometricPrompt manages the bridge in different states and finally calls the callback methods in the onResume state (when back to the activity after leaving credential window) if needed. When there are multiple LifecycleObserver, The first one will handle the result and reset the bridge, so there is nothing to do by other observers. This the reason that the first callback implementation calls twice in your code.

    Solution: You should remove LifecycleObserver from activity when you create a new instance of BiometricPrompt class. Since there is no direct access to the observer, you need use reflection here. I modified your code based on this solution as below:

    class MainActivity : AppCompatActivity() {
    
        private val TAG = MainActivity::class.java.name
        private var lastLifecycleObserver: LifecycleObserver? = null
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            findViewById<View>(R.id.first).setOnClickListener {
                authenticate(MyData(1, "first"))
            }
    
            findViewById<View>(R.id.second).setOnClickListener {
                authenticate(MyData(2, "second"))
            }
        }
    
        private fun authenticate(data: MyData) {
            Log.e(TAG, "starting auth with $data")
            lastLifecycleObserver?.let {
                lifecycle.removeObserver(it)
                lastLifecycleObserver = null
            }
            val biometricPrompt = BiometricPrompt(
                    this,
                    ContextCompat.getMainExecutor(this),
                    object : BiometricPrompt.AuthenticationCallback() {
                        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                            Log.e(TAG, "auth done : $data")
                        }
                    })
    
            var field = BiometricPrompt::class.java.getDeclaredField("mLifecycleObserver")
            field.isAccessible = true
            lastLifecycleObserver = field.get(biometricPrompt) as LifecycleObserver
    
            val promptInfo = BiometricPrompt.PromptInfo.Builder()
                    .setDeviceCredentialAllowed(true)
                    .setTitle("title")
                    .build()
            biometricPrompt.authenticate(promptInfo)
        }
    }
    
    data class MyData(
            val id: Int,
            val text: String
    )