Android Biometric Authentication offers a secure and convenient method for verifying user identity through fingerprints, facial recognition or iris scanning. This allows users to use the app without having to remember username and password every time they open the app. You can notice this in popular apps like Google Pay, PhonePe, WhatsApp and in few Banking apps.
Let's get started with some basics of biometric authentication.
Here we are using two types of authenticators
This will display the system's biometric dialog. You can customise the title, description displayed on this dialog.
Cheers!
Happy Coding 🤗
Let's get started with some basics of biometric authentication.
1. Check the device support
Before using the biometric authentication, we need to check whether the device supports it or not. This can be done by calling canAuthenticate() method from BiometricManager. If this returns BIOMETRIC_SUCCESS, we can use the biometric authentication on the device.Here we are using two types of authenticators
- BIOMETRIC_STRONG - Authenticate using any biometric method
- DEVICE_CREDENTIAL - Authenticate using device credentials like PIN, pattern or the password
const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL
fun canAuthenticate(context: Context) = BiometricManager.from(context)
.canAuthenticate(AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS
2. Enroll to Biometric Authentication
If canAuthenticate() returns BIOMETRIC_ERROR_NONE_ENROLLED, that means device supports it but user hasn't enorlled any biometric authentication method yet. To start the enroll process, we can start an Intent with Settings.ACTION_BIOMETRIC_ENROLL.
private val enrollBiometricRequestLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
// Biometric enrollment is successful. We can show the biometric login dialog
showBiometricPrompt()
} else {
Log.e(
TAG,
"Failed to enroll in biometric authentication. Error code: ${it.resultCode}"
)
}
}
fun isEnrolledPending(context: Context) = BiometricManager.from(context)
.canAuthenticate(AUTHENTICATORS) == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
private fun enrollBiometric() {
// Biometric is supported from Android 11 / Api level 30
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
enrollBiometricRequestLauncher.launch(
Intent(Settings.ACTION_BIOMETRIC_ENROLL).putExtra(
EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BiometricUtils.AUTHENTICATORS
)
)
}
}
3. Showing the biometric login dialog
To show the biometric login dialog, we need to construct BiometricPrompt by passing list of authenticators we want to use using setAllowedAuthenticators() method.
val promptInfo = BiometricUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo)
fun createPromptInfo(activity: AppCompatActivity): BiometricPrompt.PromptInfo =
BiometricPrompt.PromptInfo.Builder().apply {
setTitle(activity.getString(R.string.prompt_info_title))
setSubtitle(activity.getString(R.string.prompt_info_subtitle))
setAllowedAuthenticators(AUTHENTICATORS)
setConfirmationRequired(false)
}.build()
4. Example App
As we have covered the basics, let's implement these in a simple app. In your Android Studio create a new project. While creating I have selected Bottom Navigation View Activity to have a working bottom navigation app.- This app will check for biometric support and will prompt the login only when the device supports it
- It will start the enrollment process if user hasn't added any biometric or device credentials yet.
- If the biometric authentication is enabled, it will display a non-dismissible dialog prompting users to unlock the app using biometric authentication. If the user denies it, the dialog will block the UI until user authenticates
- Additionally, when app is kept in background for a certain duration (say 30 secs), the app will be locked and user has to authenticate again when the app is brought to foreground.
- Open app's build.gradle and add the biometric dependency.
dependencies { ... implementation "androidx.biometric:biometric:1.2.0-alpha05" ... }
- Add the below strings to your strings.xml
<resources> <string name="app_name">Biometric Authentication</string> <string name="action_settings">Settings</string> <string name="unlock">Unlock</string> <string name="prompt_info_title">App is locked</string> <string name="prompt_info_subtitle">Authentication is required to access the app</string> <string name="prompt_info_description">Sample App is using Android biometric authentication</string> <string name="enroll_biometric">Enroll Biometric</string> <string name="title_home">Home</string> <string name="title_dashboard">Dashboard</string> <string name="title_notifications">Notifications</string> <string name="error_auth_error">Authentication error. Error code: %d, Message: %s</string> <string name="locked">Locked</string> <string name="locked_message">Unlock with biometric</string> <string name="btn_unlock">Unlock Now</string> <string name="preference_file_key">shared_prefs</string> <string name="last_authenticate_time">last_authenticated_time</string> <string name="enroll_biometric_message">Biometric authentication is not enabled. Do you want to enroll now?</string> <string name="proceed">Proceed</string> <string name="cancel">Cancel</string> <string name="auth_failed">Authenticated failed!</string> </resources>
- Create a new class file named BiometricUtils.kt and add the below code. In this object class, we define all the biometric related utility methods.
BiometricUtils.kt
package info.androidhive.biometricauthentication.utils import android.content.Context import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import info.androidhive.biometricauthentication.R object BiometricUtils { const val AUTHENTICATORS = BIOMETRIC_STRONG or DEVICE_CREDENTIAL fun createBiometricPrompt( activity: AppCompatActivity, callback: BiometricAuthCallback ): BiometricPrompt { val executor = ContextCompat.getMainExecutor(activity) val promptCallback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationError(errCode: Int, errString: CharSequence) { super.onAuthenticationError(errCode, errString) callback.onAuthenticationError(errCode, errString) } override fun onAuthenticationFailed() { super.onAuthenticationFailed() callback.onAuthenticationFailed() } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) callback.onAuthenticationSucceeded(result) } } return BiometricPrompt(activity, executor, promptCallback) } fun canAuthenticate(context: Context) = BiometricManager.from(context) .canAuthenticate(AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS fun isEnrolledPending(context: Context) = BiometricManager.from(context) .canAuthenticate(AUTHENTICATORS) == BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED fun createPromptInfo(activity: AppCompatActivity): BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder().apply { setTitle(activity.getString(R.string.prompt_info_title)) setSubtitle(activity.getString(R.string.prompt_info_subtitle)) setAllowedAuthenticators(AUTHENTICATORS) setConfirmationRequired(false) }.build() interface BiometricAuthCallback { fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) fun onAuthenticationFailed() fun onAuthenticationError(errCode: Int, errString: CharSequence) } }
- Finally open the MainActivity and do the following changes.
- In onCreate(), if user hasn't added the biometric or device credentials, we start the enrollement of biometric authetication using showEnrollBiometricDialog() method
- In onResume(), showBiometricAuthIfNeeded() method is called to show the biometric login dialog if needed. This method checks few conditions like device support, the app's background state duration and displays the login prompt if app is kept in background for 30secs
- In onPause(), we store the timestamp when the app goes to background
- showLockedDialog() displays a non-dismissible dialog with Unlock option that triggers the biometric login prompt
MainActivity.ktpackage info.androidhive.biometricauthentication.main import android.app.Activity import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.os.Build import android.os.Bundle import android.provider.Settings import android.provider.Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED import android.util.Log import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricPrompt import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.dialog.MaterialAlertDialogBuilder import info.androidhive.biometricauthentication.R import info.androidhive.biometricauthentication.databinding.ActivityMainBinding import info.androidhive.biometricauthentication.utils.BiometricUtils import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity(), BiometricUtils.BiometricAuthCallback { private val TAG = "MainActivity" private lateinit var biometricPrompt: BiometricPrompt private lateinit var binding: ActivityMainBinding private lateinit var unlockDialog: AlertDialog private lateinit var sharedPref: SharedPreferences // App will ask for biometric auth after 10secs in background private val idleDuration = 30 //secs private val enrollBiometricRequestLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { showBiometricPrompt() } else { Log.e( TAG, "Failed to enroll in biometric authentication. Error code: ${it.resultCode}" ) Toast.makeText( this, "Failed to enroll in biometric authentication. Error code: ${it.resultCode}", Toast.LENGTH_LONG ).show() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) sharedPref = getSharedPreferences( getString(R.string.preference_file_key), Context.MODE_PRIVATE ) val navView: BottomNavigationView = binding.content.navView val navController = findNavController(R.id.nav_host_fragment_activity_main2) // Passing each menu ID as a set of Ids because each // menu should be considered as top level destinations. val appBarConfiguration = AppBarConfiguration( setOf( R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications ) ) setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) biometricPrompt = BiometricUtils.createBiometricPrompt(this, this) // Show enroll biometric dialog if none is enabled if (BiometricUtils.isEnrolledPending(applicationContext)) { // biometric is not enabled. User can enroll the biometric showEnrollBiometricDialog() } } private fun enrollBiometric() { // Biometric is supported from Android 11 / Api level 30 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { enrollBiometricRequestLauncher.launch( Intent(Settings.ACTION_BIOMETRIC_ENROLL).putExtra( EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, BiometricUtils.AUTHENTICATORS ) ) } } private fun showBiometricPrompt() { val promptInfo = BiometricUtils.createPromptInfo(this) biometricPrompt.authenticate(promptInfo) } override fun onPause() { super.onPause() // Save timestamp when app kept in background with(sharedPref.edit()) { putLong(getString(R.string.last_authenticate_time), System.currentTimeMillis()) apply() } } override fun onResume() { super.onResume() // show biometric dialog when app is resumed from background showBiometricAuthIfNeeded() } /* * Show biometric auth if needed * Shows biometric auth if app kept in background more than 10secs * */ private fun showBiometricAuthIfNeeded() { if (BiometricUtils.canAuthenticate(applicationContext)) { val lastMilliSec = sharedPref.getLong(getString(R.string.last_authenticate_time), -1) if (lastMilliSec.toInt() == -1) { showBiometricPrompt() return } // seconds difference between now and app background state time val secs = TimeUnit.MILLISECONDS.toSeconds( System.currentTimeMillis() - sharedPref.getLong( getString(R.string.last_authenticate_time), 0 ) ) Log.d( TAG, "Secs $secs, ${ sharedPref.getLong( getString(R.string.last_authenticate_time), 0 ) }" ) // show biometric dialog if app idle time is more than 10secs if (secs > idleDuration) { showBiometricPrompt() } } } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { dismissUnlockDialog() // store last authenticated timestamp with(sharedPref.edit()) { putLong(getString(R.string.last_authenticate_time), System.currentTimeMillis()) apply() } } override fun onAuthenticationFailed() { Toast.makeText(this, R.string.auth_failed, Toast.LENGTH_SHORT).show() } override fun onAuthenticationError(errCode: Int, errString: CharSequence) { Log.e(TAG, "Authenticated error. Error code: $errCode, Message: $errString") // Show unlock dialog if user cancels auth dialog if (errCode == BiometricPrompt.ERROR_USER_CANCELED) { showLockedDialog() } else { // authentication error dismissUnlockDialog() Toast.makeText( this, getString(R.string.error_auth_error, errCode, errString), Toast.LENGTH_LONG ).show() } } // Enroll into biometric dialog private fun showEnrollBiometricDialog() { val dialog = MaterialAlertDialogBuilder(this) .setTitle(R.string.enroll_biometric) .setMessage(R.string.enroll_biometric_message) .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } .setPositiveButton(R.string.proceed) { _, _ -> enrollBiometric() }.setCancelable(false) unlockDialog = dialog.show() } // Show app locked dialog private fun showLockedDialog() { val dialog = MaterialAlertDialogBuilder(this).setTitle(R.string.locked) .setMessage(R.string.locked_message).setPositiveButton(R.string.btn_unlock) { _, _ -> showBiometricPrompt() }.setCancelable(false) unlockDialog = dialog.show() } private fun dismissUnlockDialog() { if (this::unlockDialog.isInitialized && unlockDialog.isShowing) unlockDialog.dismiss() } }
Cheers!
Happy Coding 🤗
Informative article. Thank you.
ReplyDeleteYou are welcome :)
DeleteAwesome article! Clear steps and useful code for adding biometric authentication in Android apps. Thanks for sharing!
ReplyDelete