Biometric authentication has transformed mobile security, offering a seamless way to verify user identity without the friction of passwords or PINs. Modern Android devices support multiple biometric modalities including fingerprints, face recognition, and iris scanning. Implementing biometric authentication correctly requires understanding the BiometricPrompt API, integrating with the Android Keystore for cryptographic operations, and designing fallback flows for devices and situations where biometrics are unavailable.
This guide walks through implementing production-ready biometric authentication in Android applications. We cover the complete flow from checking device capabilities through cryptographic key management to handling the full spectrum of authentication outcomes.
Understanding Biometric Classes
Android categorizes biometric hardware into three classes based on security strength. Understanding these classes helps you make appropriate security decisions for your application.
Class 3 (Strong) biometrics meet the highest security standards with a spoof acceptance rate below 7% and require secure hardware implementation. Fingerprint sensors and advanced face recognition systems typically achieve Class 3 certification. These biometrics can be used for any authentication purpose, including unlocking cryptographic keys.
Class 2 (Weak) biometrics have a spoof acceptance rate below 20% but may not use secure hardware. Basic face unlock implementations often fall into this category. These can authenticate users for convenience features but should not protect highly sensitive operations.
Class 1 (Convenience) biometrics have no formal security requirements and exist purely for user convenience. They should never be used for security-sensitive operations.
The BiometricManager class lets you query what biometric capabilities a device supports and their security classes. Your application should check capabilities and adjust its security posture accordingly.
val biometricManager = BiometricManager.from(context)
fun checkBiometricCapability(): BiometricCapability {
return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS ->
BiometricCapability.StrongAvailable
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
BiometricCapability.NoHardware
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
BiometricCapability.HardwareUnavailable
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED ->
BiometricCapability.NoneEnrolled
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED ->
BiometricCapability.SecurityUpdateRequired
else -> BiometricCapability.Unknown
}
}
sealed class BiometricCapability {
object StrongAvailable : BiometricCapability()
object NoHardware : BiometricCapability()
object HardwareUnavailable : BiometricCapability()
object NoneEnrolled : BiometricCapability()
object SecurityUpdateRequired : BiometricCapability()
object Unknown : BiometricCapability()
}
Setting Up BiometricPrompt
BiometricPrompt provides a system-managed UI for biometric authentication. The system handles sensor interaction, displays appropriate prompts, and manages error states. Your application receives callbacks indicating success, failure, or errors.
Creating a BiometricPrompt requires an executor for callback delivery and an AuthenticationCallback implementation. The callback receives the authentication result and must handle all possible outcomes.
class BiometricAuthenticator(
private val activity: FragmentActivity
) {
private val executor = ContextCompat.getMainExecutor(activity)
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
private var onSuccess: ((BiometricPrompt.AuthenticationResult) -> Unit)? = null
private var onError: ((Int, String) -> Unit)? = null
private var onFailed: (() -> Unit)? = null
init {
setupBiometricPrompt()
setupPromptInfo()
}
private fun setupBiometricPrompt() {
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
onSuccess?.invoke(result)
}
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString)
onError?.invoke(errorCode, errString.toString())
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
onFailed?.invoke()
}
}
biometricPrompt = BiometricPrompt(activity, executor, callback)
}
private fun setupPromptInfo() {
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setSubtitle("Verify your identity to continue")
.setNegativeButtonText("Use password")
.setAllowedAuthenticators(BIOMETRIC_STRONG)
.build()
}
fun authenticate(
onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,
onError: (Int, String) -> Unit,
onFailed: () -> Unit
) {
this.onSuccess = onSuccess
this.onError = onError
this.onFailed = onFailed
biometricPrompt.authenticate(promptInfo)
}
}
The PromptInfo builder configures the authentication dialog appearance and behavior. You can customize the title, subtitle, and description. The negative button provides an escape hatch when biometric authentication is not working or the user prefers an alternative method.
Cryptographic Integration with Android Keystore
Simple biometric authentication verifies that someone with enrolled biometrics is present, but it does not cryptographically prove anything. For stronger security, bind cryptographic keys to biometric authentication so that keys can only be used after successful biometric verification.
The Android Keystore stores cryptographic keys in secure hardware when available. Keys can be configured to require user authentication before use. When biometric authentication succeeds, the system unlocks the key for a configured duration.
class BiometricCryptoManager(private val context: Context) {
companion object {
private const val KEY_NAME = "biometric_key"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
}
private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
load(null)
}
fun generateKey(): Boolean {
return try {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE
)
val keySpec = KeyGenParameterSpec.Builder(
KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setUserAuthenticationRequired(true)
.setUserAuthenticationParameters(
0, // Authentication valid for single use
KeyProperties.AUTH_BIOMETRIC_STRONG
)
.setInvalidatedByBiometricEnrollment(true)
.build()
keyGenerator.init(keySpec)
keyGenerator.generateKey()
true
} catch (e: Exception) {
false
}
}
fun getCipher(): Cipher? {
return try {
val key = keyStore.getKey(KEY_NAME, null) as? SecretKey
?: return null
Cipher.getInstance(
"${KeyProperties.KEY_ALGORITHM_AES}/" +
"${KeyProperties.BLOCK_MODE_GCM}/" +
KeyProperties.ENCRYPTION_PADDING_NONE
).apply {
init(Cipher.ENCRYPT_MODE, key)
}
} catch (e: KeyPermanentlyInvalidatedException) {
// Key invalidated due to biometric enrollment change
keyStore.deleteEntry(KEY_NAME)
null
} catch (e: Exception) {
null
}
}
fun encrypt(cipher: Cipher, data: ByteArray): EncryptedData {
val encryptedBytes = cipher.doFinal(data)
return EncryptedData(
ciphertext = encryptedBytes,
iv = cipher.iv
)
}
fun decrypt(encryptedData: EncryptedData): ByteArray? {
return try {
val key = keyStore.getKey(KEY_NAME, null) as? SecretKey
?: return null
val cipher = Cipher.getInstance(
"${KeyProperties.KEY_ALGORITHM_AES}/" +
"${KeyProperties.BLOCK_MODE_GCM}/" +
KeyProperties.ENCRYPTION_PADDING_NONE
)
val spec = GCMParameterSpec(128, encryptedData.iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
cipher.doFinal(encryptedData.ciphertext)
} catch (e: Exception) {
null
}
}
}
data class EncryptedData(
val ciphertext: ByteArray,
val iv: ByteArray
)
When authenticating with a CryptoObject, the biometric prompt unlocks the key for use. The cipher returned in the authentication result can then perform cryptographic operations.
fun authenticateWithCrypto(
cryptoManager: BiometricCryptoManager,
onAuthenticated: (Cipher) -> Unit,
onError: (String) -> Unit
) {
val cipher = cryptoManager.getCipher()
if (cipher == null) {
// Key does not exist or was invalidated
if (cryptoManager.generateKey()) {
// Retry with new key
authenticateWithCrypto(cryptoManager, onAuthenticated, onError)
} else {
onError("Failed to create encryption key")
}
return
}
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
biometricPrompt.authenticate(promptInfo, cryptoObject)
// In the success callback:
// result.cryptoObject?.cipher?.let { onAuthenticated(it) }
}
Handling Authentication Outcomes
Biometric authentication can fail in many ways, and your application must handle each appropriately. The distinction between failure and error is important: failure means the biometric did not match (user might try again), while error means authentication cannot proceed.
Common error codes include BIOMETRIC_ERROR_LOCKOUT when too many failed attempts disable biometrics temporarily, BIOMETRIC_ERROR_LOCKOUT_PERMANENT when biometrics are disabled until the device is unlocked with PIN/password, and BIOMETRIC_ERROR_USER_CANCELED when the user dismissed the prompt.
private fun handleAuthenticationError(errorCode: Int, message: String) {
when (errorCode) {
BiometricPrompt.ERROR_LOCKOUT -> {
// Too many failed attempts, temporary lockout
showMessage("Too many attempts. Try again in 30 seconds.")
offerPasswordFallback()
}
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {
// Device must be unlocked with PIN/password
showMessage("Biometric locked. Please use your device PIN.")
offerDeviceCredentialFallback()
}
BiometricPrompt.ERROR_USER_CANCELED -> {
// User pressed the negative button or dismissed
offerPasswordFallback()
}
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
// User pressed "Use password" button
navigateToPasswordEntry()
}
BiometricPrompt.ERROR_NO_BIOMETRICS -> {
// No biometrics enrolled
showMessage("No biometrics enrolled. Please use password.")
offerPasswordFallback()
}
BiometricPrompt.ERROR_HW_NOT_PRESENT,
BiometricPrompt.ERROR_HW_UNAVAILABLE -> {
// Hardware issue
showMessage("Biometric hardware unavailable.")
offerPasswordFallback()
}
else -> {
showMessage(message)
offerPasswordFallback()
}
}
}
Device Credential Fallback
Some applications allow authentication using the device lock screen credential (PIN, pattern, or password) as an alternative to biometrics. This is convenient but carries different security implications since the credential is shared across all device security contexts.
Configure the BiometricPrompt to accept device credentials by setting the appropriate authenticator types. When device credentials are allowed, the negative button is replaced by a device credential option.
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setSubtitle("Use biometrics or device PIN")
.setAllowedAuthenticators(
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
)
// Note: setNegativeButtonText cannot be used with DEVICE_CREDENTIAL
.build()
Best Practices for Production Applications
Several practices distinguish production-quality biometric implementations from basic examples.
Always provide fallback authentication. Biometrics can be unavailable for many reasons: temporary lockouts, hardware failures, wet fingers, poor lighting for face recognition, or users who simply prefer passwords. Applications that require biometrics without fallback frustrate users and may become completely inaccessible.
Handle key invalidation gracefully. When users enroll new biometrics, keys configured with setInvalidatedByBiometricEnrollment(true) become permanently unusable. Detect this condition and re-establish credentials using your fallback authentication method.
Consider the user experience carefully. Biometric prompts interrupting other interactions feel jarring. Trigger authentication at natural points in the user flow, such as when accessing sensitive data or confirming transactions.
Test on real devices with various biometric types. Emulators cannot replicate the full biometric experience. Different devices have different sensor characteristics and failure modes that only appear with physical testing.
Respect user preferences. Some users prefer passwords for privacy or accessibility reasons. Others may have medical conditions affecting biometric reliability. Offering choice and remembering preferences improves the experience for everyone.
Security Considerations
Biometric authentication is powerful but not infallible. Sophisticated attackers can potentially spoof biometrics, and the non-revocability of biometrics (you cannot change your fingerprint like a password) means compromise has permanent implications.
For highly sensitive operations, consider requiring both biometric and knowledge-based authentication. A fingerprint plus a PIN provides defense in depth that neither factor alone achieves.
Never store biometric data in your application. The Android biometric APIs handle all sensor interaction and template matching. Your application receives only a success or failure indication, never the actual biometric data.
Be cautious with biometric-protected keys. If an attacker gains access to an unlocked device, they may be able to perform biometric authentication if the legitimate user’s biometrics are still registered. Time-limited key validity and transaction-specific authentication help mitigate this risk.
Conclusion
Biometric authentication, when implemented correctly, provides an excellent balance of security and convenience. The BiometricPrompt API abstracts away hardware differences and provides consistent behavior across devices. Integration with the Android Keystore enables cryptographically strong authentication that binds secrets to biometric verification.
The implementation patterns shown here form the foundation of production biometric systems. Adapt them to your specific security requirements, always provide fallback options, and test thoroughly on physical devices.
At RyuPy, biometric authentication is a key component of our security-conscious applications. We believe that security should not come at the cost of usability, and modern biometrics exemplify this principle when implemented thoughtfully.

0 Comments