Skip to content
intermediate Phase 12 · Security

Biometric Authentication

Integrate fingerprint and face authentication with the Biometric API.

40m
2 problems
Topic Progress 0%

The AndroidX Biometric API

Why Biometrics?

Passwords and PINs are easily forgotten or observed. Biometric authentication—fingerprint, face, iris—provides a more natural and secure way to verify user identity. Android's biometric framework ties authentication to hardware-backed cryptographic keys, ensuring that biometric data never leaves the secure enclave.

AndroidX Biometric Library

The AndroidX Biometric library (androidx.biometric) provides a consistent API across devices and Android versions. It replaces the deprecated FingerprintManager and FaceManager APIs.

// build.gradle.kts
implementation("androidx.biometric:biometric:1.2.0-alpha05")

BiometricPrompt vs BiometricManager

  • BiometricManager — Checks if the device supports biometrics and if the user has enrolled them
  • BiometricPrompt — The actual authentication dialog and flow
val biometricManager = BiometricManager.from(context)

when (biometricManager.canAuthenticate(
    BiometricManager.Authenticators.BIOMETRIC_STRONG
)) {
    BiometricManager.BIOMETRIC_SUCCESS -> {
        // Biometrics available and enrolled
    }
    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
        // No biometric hardware
    }
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
        // No biometrics enrolled, prompt user to enroll
    }
    BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
        // Security update needed
    }
    BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
        // Biometrics not supported
    }
    BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
        // Unknown status
    }
}

Authentication Strength Levels

Level Authenticator Security
BIOMETRIC_STRONG Fingerprint, Face (with liveness) Hardware-backed, suitable for payment and sensitive operations
BIOMETRIC_WEAK Basic face recognition Software-level, suitable for app convenience features
DEVICE_CREDENTIAL PIN, pattern, password Fallback when biometrics are unavailable

Always prefer BIOMETRIC_STRONG for authentication that protects sensitive data or operations.

Implementing BiometricPrompt

Basic Authentication Flow

class LoginActivity : AppCompatActivity() {

    private val executor = ContextCompat.getMainExecutor(this)

    private val biometricPrompt = BiometricPrompt(this, executor,
        object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                navigateToMainScreen()
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                when (errorCode) {
                    BiometricPrompt.ERROR_NEGATIVE_BUTTON,
                    BiometricPrompt.ERROR_USER_CANCELED -> {
                        // User cancelled — do nothing
                    }
                    BiometricPrompt.ERROR_LOCKOUT -> {
                        showMessage("Too many attempts. Try again later.")
                    }
                    BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {
                        showMessage("Biometric locked. Use device PIN.")
                    }
                    else -> showMessage(errString.toString())
                }
            }

            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                // Fingerprint not recognized — prompt shows retry automatically
            }
        }
    )

    fun showBiometricPrompt() {
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Verify Your Identity")
            .setSubtitle("Scan your fingerprint to continue")
            .setNegativeButtonText("Use Password")
            .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
            .build()

        biometricPrompt.authenticate(promptInfo)
    }
}

Combining Biometrics with Device Credential

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Verify Your Identity")
    .setSubtitle("Use fingerprint, face, or your device PIN")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG or
        BiometricManager.Authenticators.DEVICE_CREDENTIAL
    )
    .build()

// Note: setNegativeButtonText is NOT allowed when DEVICE_CREDENTIAL is included
biometricPrompt.authenticate(promptInfo)

CryptoObject for Key-Bound Authentication

For maximum security, bind the biometric authentication to a cryptographic operation. The key is only released from the Android Keystore after successful biometric verification.

private fun createCryptoObject(): BiometricPrompt.CryptoObject {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")
    keyStore.load(null)

    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    val keyGenSpec = KeyGenParameterSpec.Builder(
        "biometric_key",
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setInvalidatedByBiometricEnrollment(true)
        .build()

    keyGenerator.init(keyGenSpec)
    keyGenerator.generateKey()

    val key = keyStore.getKey("biometric_key", null) as SecretKey
    val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
    cipher.init(Cipher.ENCRYPT_MODE, key)

    return BiometricPrompt.CryptoObject(cipher)
}

fun showBiometricPromptWithCrypto() {
    val cryptoObject = createCryptoObject()
    val promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Authenticate to Decrypt")
        .setNegativeButtonText("Cancel")
        .build()

    biometricPrompt.authenticate(promptInfo, cryptoObject)
}

The setInvalidatedByBiometricEnrollment(true) flag invalidates the key if the user adds or removes a biometric, preventing stale keys from being used.

Enrollment, Fallback, and Edge Cases

Handling No Biometric Enrollment

Always check if biometrics are enrolled before showing the prompt. If not, guide the user to device settings.

if (biometricManager.canAuthenticate(BIOMETRIC_STRONG) == 
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED) {
    
    AlertDialog.Builder(this)
        .setTitle("Biometrics Not Set Up")
        .setMessage("Please set up fingerprint or face unlock in device settings.")
        .setPositiveButton("Open Settings") { _, _ ->
            val intent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
                putExtra(
                    Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                    KeyguardManager.BIOMETRIC_STRONG
                )
            }
            startActivity(intent)
        }
        .setNegativeButton("Use Password") { _, _ ->
            loginWithPassword()
        }
        .show()
}

Cancellation Handling

Users can cancel the biometric prompt by tapping outside or pressing back. Handle this gracefully:

// Track whether authentication was cancelled vs failed
private var authCancelled = false

private val biometricPrompt = BiometricPrompt(this, executor,
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
                errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
                authCancelled = true
                // Don't show error — user chose to cancel
            }
        }
    }
)

Key Invalidation After Biometric Changes

When setInvalidatedByBiometricEnrollment(true) is set, the key is invalidated if:

  • A new fingerprint is enrolled
  • A new face is enrolled
  • All biometrics of one type are removed

This means your app must handle re-encryption after biometric changes:

try {
    val cipher = createDecryptCipher()
    val data = decryptData(encryptedData, cipher)
    // Success
} catch (e: UserNotAuthenticatedException) {
    // Key was invalidated — re-authenticate and re-encrypt
    showBiometricPrompt()
} catch (e: KeyPermanentlyInvalidatedException) {
    // All biometrics removed — generate new key and re-encrypt
    recreateBiometricKey()
}

Interview Tips

  • Difference between CryptoObject and no CryptoObject: CryptoObject ties the biometric auth to a cryptographic key, providing stronger security. Without it, biometrics only gates access to code logic.
  • Why setInvalidatedByBiometricEnrollment matters: Prevents keys from being used after the user's biometric profile changes.
  • Security model: Biometric data never leaves the secure enclave. The framework only returns a success/failure signal.

Quiz

1. What is the difference between BIOMETRIC_STRONG and BIOMETRIC_WEAK?

Question 1 options

2. Why use a CryptoObject with BiometricPrompt?

Question 2 options

3. What happens to the biometric-bound key when the user enrolls a new fingerprint?

Question 3 options

4. Can BiometricPrompt use DEVICE_CREDENTIAL alongside biometrics?

Question 4 options

Flashcards

Question

What is the purpose of BiometricPrompt.CryptoObject?

Answer

It wraps a cryptographic object (cipher, MAC, or signature) and ensures the key is only released from the Android Keystore after successful biometric authentication.

Question

What does setInvalidatedByBiometricEnrollment do?

Answer

When true, the bound cryptographic key is invalidated if the user adds or removes biometrics, preventing stale keys from being used with different biometric profiles.

Question

How do you check if biometrics are available on a device?

Answer

Use BiometricManager.from(context).canAuthenticate() which returns BIOMETRIC_SUCCESS, BIOMETRIC_ERROR_NO_HARDWARE, BIOMETRIC_ERROR_NONE_ENROLLED, or other error codes.

Question

Why should BIOMETRIC_STRONG be preferred over BIOMETRIC_WEAK?

Answer

BIOMETRIC_STRONG requires hardware-backed security and liveness detection, making it suitable for payment and sensitive operations. BIOMETRIC_WEAK allows software-level matching with lower security guarantees.

Revision Notes

Key Takeaways

  • 1. Use AndroidX Biometric library for cross-device compatibility, not the deprecated FingerprintManager
  • 2. BIOMETRIC_STRONG with CryptoObject provides the highest security level for payment and auth flows
  • 3. setInvalidatedByBiometricEnrollment invalidates keys when biometric profiles change
  • 4. Always check BiometricManager.canAuthenticate() before attempting biometric authentication
  • 5. Provide a device credential fallback for users without biometrics enrolled

Interview Tips

  • Explain how CryptoObject binds biometric auth to the Android Keystore
  • Know the difference between BIOMETRIC_STRONG and BIOMETRIC_WEAK
  • Be ready to discuss key invalidation after biometric enrollment changes
  • Explain why biometric data never leaves the secure enclave

Cheat Sheet

Biometric Authentication Cheat Sheet

AndroidX Biometric:

  • BiometricManager — check availability
  • BiometricPrompt — show auth dialog
  • CryptoObject — bind to cryptographic key

Strength Levels:

  • BIOMETRIC_STRONG — hardware-backed, liveness detection
  • BIOMETRIC_WEAK — software-level matching
  • DEVICE_CREDENTIAL — PIN/pattern/password fallback

Key Config:

  • setUserAuthenticationRequired(true) — key needs biometric
  • setInvalidatedByBiometricEnrollment(true) — invalidate on biometric changes

Error Codes:

  • ERROR_LOCKOUT — too many failed attempts
  • ERROR_LOCKOUT_PERMANENT — permanently locked
  • ERROR_USER_CANCELED — user dismissed prompt

Best Practices:

  • Check canAuthenticate() before showing prompt
  • Handle ERROR_NONE_ENROLLED by guiding to settings
  • Use CryptoObject for sensitive operations
  • Always provide a non-biometric fallback