1. Introduction
What ou will learn
Biometric login provides a convenient method for authorizing access to private content within your app. Instead of having to remember an account username and password every time they open your app, users can just use their biometric credentials to confirm their presence and authorize access to the private content.
Figure 1
What You Will Need
- A recent version of Android Studio (>= 3.6)
- An Android device that's running Android 8.0 (Oreo) or greater and that has a biometric sensor – emulators won't work since they don't have a keystore
- Moderate knowledge of Android development
- Ability to read and understand Kotlin code
What You Will Build
You are going to add biometric authentication to an existing app that currently requires frequent user login. This new functionality will make login more convenient for your users.
- Start with an app that has a typical login Activity (provided for you).
- Add a button that gives users the option to "use biometric" authentication.
- Create a biometric authorization Activity to associate a server-generated user token with the user's biometric credentials.
- In the login Activity, add logic to ask the user to login with biometrics.
Get the Code From Github
The code to get started is stored in a GitHub repository. You can clone the repository via the following command:
git clone
https://github.com/android/codelab-biometric-login.git
Alternatively, you can download the repository as a ZIP file and extract it locally:
Directory Structure
After you've cloned or unzipped from Github, you'll end up with the root directory biometric-login-kotlin
. The root directory contains the following folders:
/PATH/TO/YOUR/FOLDER/codelab-biometric-login/codelab-00
/PATH/TO/YOUR/FOLDER/codelab-biometric-login/codelab-01
/PATH/TO/YOUR/FOLDER/codelab-biometric-login/codelab-02
Each folder is an independent Android Studio project. The codelab-00
project contains the source that we'll use as our starting point. The optional codelab-NN
projects contain the expected project state after each major section in this codelab. You can use these optional projects to check your work along the way.
2. Codelab-00: The Groundwork
Import the Project into Android Studio
Codelab-00
is the base app that doesn't contain any biometric capabilities. Start Android Studio and import codelab-00
by choosing File -> New -> Import Project.... After Android Studio builds the project, attach a device via USB and run the app. You'll see a screen similar to Figure 2.
Figure 2
The app consists of five class files: LoginActivity
, LoginResult
, LoginState
LoginViewModel
, SampleAppUser
.
Gradle
You need to add a Gradle dependency in order to use the Android Biometric Library in your app. Open the build.gradle
file of the app
module, and add the following:
dependencies {
...
implementation "androidx.biometric:biometric:1.0.1"
...
}
How Biometric Login Works
During username-password authentication, the app sends the user's credentials to a remote server and the server returns a user token. That server-generated token may be kept in memory until the user closes the app. After some time, when the user opens the app again, they may need to login again.
For biometric authentication the flow is a little different. You will need to add a "use biometrics" UI to the login page. The very first time the user clicks on the "use biometrics" UI, the app will prompt the user to enable biometric authentication in the app. On the "enable" page, the user will enter a username-password combination as usual, and the credentials will be sent to the remote server as usual. But this time when the server returns the user token, the app will encrypt the token using a secret key backed by the user's biometrics and then store the encrypted token on disk. Next time the user needs to login, instead of asking the server for the token, they can decrypt the stored token using their biometrics.
Setting Up for the Biometric Login
A few objects have to be in place before you can display the "use biometrics" UI.
- First we will set up a
CryptographyManager
class to handle encryption, decryption and storage of the user token. - Then, since both
LoginActivity
andEnableBiometricLoginActivity
need to callBiometricPrompt
, we will create aBiometricPromptUtils
file for the shared code. - Finally we will create the "use biometrics" UI and wire it to handle the different behaviors.
CryptographyManager
The API for adding biometric authentication to your app is called BiometricPrompt
. In this codelab, the BiometricPrompt uses a CryptoObject
to communicate with the system that performs encryption and decryption on Android. A CryptoObject
requires a Cipher
, a MAC
, a Signature
, or an IdentityCredential
as parameters. For this exercise, you will pass it a Cipher
.
Create a file called CryptographyManager.kt
and add the following content to it. In addition to providing a Cipher
plus encryption and decryption functions, this file also provides functions to store and retrieve the server-generated user token.
package com.example.biometricloginsample
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.google.gson.Gson
import java.nio.charset.Charset
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
/**
* Handles encryption and decryption
*/
interface CryptographyManager {
fun getInitializedCipherForEncryption(keyName: String): Cipher
fun getInitializedCipherForDecryption(keyName: String, initializationVector: ByteArray): Cipher
/**
* The Cipher created with [getInitializedCipherForEncryption] is used here
*/
fun encryptData(plaintext: String, cipher: Cipher): CiphertextWrapper
/**
* The Cipher created with [getInitializedCipherForDecryption] is used here
*/
fun decryptData(ciphertext: ByteArray, cipher: Cipher): String
fun persistCiphertextWrapperToSharedPrefs(
ciphertextWrapper: CiphertextWrapper,
context: Context,
filename: String,
mode: Int,
prefKey: String
)
fun getCiphertextWrapperFromSharedPrefs(
context: Context,
filename: String,
mode: Int,
prefKey: String
): CiphertextWrapper?
}
fun CryptographyManager(): CryptographyManager = CryptographyManagerImpl()
/**
* To get an instance of this private CryptographyManagerImpl class, use the top-level function
* fun CryptographyManager(): CryptographyManager = CryptographyManagerImpl()
*/
private class CryptographyManagerImpl : CryptographyManager {
private val KEY_SIZE = 256
private val ANDROID_KEYSTORE = "AndroidKeyStore"
private val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
private val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
private val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
override fun getInitializedCipherForEncryption(keyName: String): Cipher {
val cipher = getCipher()
val secretKey = getOrCreateSecretKey(keyName)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
return cipher
}
override fun getInitializedCipherForDecryption(
keyName: String,
initializationVector: ByteArray
): Cipher {
val cipher = getCipher()
val secretKey = getOrCreateSecretKey(keyName)
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, initializationVector))
return cipher
}
override fun encryptData(plaintext: String, cipher: Cipher): CiphertextWrapper {
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charset.forName("UTF-8")))
return CiphertextWrapper(ciphertext, cipher.iv)
}
override fun decryptData(ciphertext: ByteArray, cipher: Cipher): String {
val plaintext = cipher.doFinal(ciphertext)
return String(plaintext, Charset.forName("UTF-8"))
}
private fun getCipher(): Cipher {
val transformation = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING"
return Cipher.getInstance(transformation)
}
private fun getOrCreateSecretKey(keyName: String): SecretKey {
// If Secretkey was previously created for that keyName, then grab and return it.
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null) // Keystore must be loaded before it can be accessed
keyStore.getKey(keyName, null)?.let { return it as SecretKey }
// if you reach here, then a new SecretKey must be generated for that keyName
val paramsBuilder = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
paramsBuilder.apply {
setBlockModes(ENCRYPTION_BLOCK_MODE)
setEncryptionPaddings(ENCRYPTION_PADDING)
setKeySize(KEY_SIZE)
setUserAuthenticationRequired(true)
}
val keyGenParams = paramsBuilder.build()
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE
)
keyGenerator.init(keyGenParams)
return keyGenerator.generateKey()
}
override fun persistCiphertextWrapperToSharedPrefs(
ciphertextWrapper: CiphertextWrapper,
context: Context,
filename: String,
mode: Int,
prefKey: String
) {
val json = Gson().toJson(ciphertextWrapper)
context.getSharedPreferences(filename, mode).edit().putString(prefKey, json).apply()
}
override fun getCiphertextWrapperFromSharedPrefs(
context: Context,
filename: String,
mode: Int,
prefKey: String
): CiphertextWrapper? {
val json = context.getSharedPreferences(filename, mode).getString(prefKey, null)
return Gson().fromJson(json, CiphertextWrapper::class.java)
}
}
data class CiphertextWrapper(val ciphertext: ByteArray, val initializationVector: ByteArray)
BiometricPrompt Utils
As mentioned earlier, let's add the BiometricPromptUtils
, which contains code that will be used by both LoginActivity
and EnableBiometricLoginActivity
. Create a file called BiometricPromptUtils.kt
and add the following content to it. This file simply factors out the steps for creating a BiometricPrompt
instance and a PromptInfo
instance.
package com.example.biometricloginsample
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
// Since we are using the same methods in more than one Activity, better give them their own file.
object BiometricPromptUtils {
private const val TAG = "BiometricPromptUtils"
fun createBiometricPrompt(
activity: AppCompatActivity,
processSuccess: (BiometricPrompt.AuthenticationResult) -> Unit
): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(activity)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errCode: Int, errString: CharSequence) {
super.onAuthenticationError(errCode, errString)
Log.d(TAG, "errCode is $errCode and errString is: $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d(TAG, "User biometric rejected.
")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "Authentication was successful")
processSuccess(result)
}
}
return BiometricPrompt(activity, executor, callback)
}
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))
setDescription(activity.getString(R.string.prompt_info_description))
setConfirmationRequired(false)
setNegativeButtonText(activity.getString(R.string.prompt_info_use_app_password))
}.build()
}
You will also need to add the following to your res/values/strings.xml
file.
<string name="prompt_info_title">Sample App Authentication</string>
<string name="prompt_info_subtitle">Please login to get access</string>
<string name="prompt_info_description">Sample App is using Android biometric authentication</string>
<string name="prompt_info_use_app_password">Use app password</string>
Finally create a Constants.kt
file and add the following content to it.
package com.example.biometricloginsample
const val SHARED_PREFS_FILENAME = "biometric_prefs"
const val CIPHERTEXT_WRAPPER = "ciphertext_wrapper"
Add Biometric Login UI
Open the res/layout/activity_login.xml
file and add a TextView
that the user can click to log in using their biometric credentials. (You will need to delete the old @+id/success
TextView)
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/use_biometrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/standard_padding"
android:layout_marginTop="16dp"
android:layout_marginRight="@dimen/standard_padding"
android:text="Use biometrics"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#0000EE"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/login" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/success"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_padding"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="@color/colorPrimaryDark"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/use_biometrics"
tools:text="@string/already_signedin" />
Your app should now look as in Figure 3. The "Use biometrics" UI is no-op for the time being. We'll add functionality to it in the following sections.
Figure 3
3. Codelab-01: Add Logic for Biometric Login
Add Biometric Authentication Wiring
Now that the prerequisites are in place, we can add biometric logic to the LoginActivity
. Recall that the "Use biometrics" UI has an initial behavior and a general behavior. When the user interacts with the UI for the first time, it prompts the user to confirm that they want to enable biometrics login for the app. To accomplish this, the UI's onClick()
method launches an intent to start the activity EnableBiometricLoginActivity
. For all subsequent times that the user sees the UI, a biometric prompt appears.
Add the following logic to the LoginActivity
to handle these behaviors. (Note that this snippet will replace your existing onCreate()
function.)
private lateinit var biometricPrompt: BiometricPrompt
private val cryptographyManager = CryptographyManager()
private val ciphertextWrapper
get() = cryptographyManager.getCiphertextWrapperFromSharedPrefs(
applicationContext,
SHARED_PREFS_FILENAME,
Context.MODE_PRIVATE,
CIPHERTEXT_WRAPPER
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
val canAuthenticate = BiometricManager.from(applicationContext).canAuthenticate()
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
binding.useBiometrics.visibility = View.VISIBLE
binding.useBiometrics.setOnClickListener {
if (ciphertextWrapper != null) {
showBiometricPromptForDecryption()
} else {
startActivity(Intent(this, EnableBiometricLoginActivity::class.java))
}
}
} else {
binding.useBiometrics.visibility = View.INVISIBLE
}
if (ciphertextWrapper == null) {
setupForLoginWithPassword()
}
}
/**
* The logic is kept inside onResume instead of onCreate so that authorizing biometrics takes
* immediate effect.
*/
override fun onResume() {
super.onResume()
if (ciphertextWrapper != null) {
if (SampleAppUser.fakeToken == null) {
showBiometricPromptForDecryption()
} else {
// The user has already logged in, so proceed to the rest of the app
// this is a todo for you, the developer
updateApp(getString(R.string.already_signedin))
}
}
}
// USERNAME + PASSWORD SECTION
For now we will keep the showBiometricPromptForDecryption()
function unimplemented.
Create EnableBiometricLoginActivity
Create an empty Activity that extends AppCompatActivity and name it EnableBiometricLoginActivity
. Change the associated xml file, res/layout/activity_enable_biometric_login.xml
, to the following.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".EnableBiometricLoginActivity">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_padding"
android:fontFamily="sans-serif-condensed-light"
android:text="@string/enable_biometric_login"
android:textAppearance="?android:attr/textAppearanceLarge"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_padding"
android:text="@string/desc_biometrics_authorization"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_padding"
android:hint="@string/username_hint"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/description" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_padding"
android:hint="@string/password"
android:imeOptions="actionDone"
android:inputType="textPassword"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/username" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_padding"
android:text="@string/cancel"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/authorize"
app:layout_constraintTop_toBottomOf="@id/password" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/authorize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_padding"
android:text="@string/btn_authorize"
app:layout_constraintLeft_toRightOf="@+id/cancel"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/password" />
</androidx.constraintlayout.widget.ConstraintLayout>
Add the following snippet to your res/values/strings.xml
resource file
<string name="enable_biometric_login">Enable Biometric Login</string>
<string name="desc_biometrics_authorization">Enter your login ID and password to confirm activation of Biometric Login.</string>
<string name="cancel">Cancel</string>
<string name="btn_authorize">Authorize</string>
Run your app. When you click on the "Use Biometrics" UI, it should take you to a screen similar to Figure 4.
Figure 4
Add Logic to EnableBiometricLoginActivity
As you can see from Figure 4, after entering the username and password, the user must click on "authorize" to enable biometric authentication. Here is what that means for your code.
- The username and password TextViews inside
EnableBiometricLoginActivity
should be wired similarly to those inside ofLoginActivity
. - Unlike
LoginActivity
, however, when the user clicks the "Authorize" button, you will launch theBiometricPrompt
. - When the
BiometricPrompt
returns, you will use the associatedCipher
to encrypt the server-generated user token. - Finally you should close
EnableBiometricLoginActivity
.
For step 1, you will just connect the LoginViewModel and let it handle the username-password authentication for you. To that end, replace your onCreate() function with the following code snippet.
private val TAG = "EnableBiometricLogin"
private lateinit var cryptographyManager: CryptographyManager
private val loginViewModel by viewModels<LoginViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityEnableBiometricLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.cancel.setOnClickListener { finish() }
loginViewModel.loginWithPasswordFormState.observe(this, Observer { formState ->
val loginState = formState ?: return@Observer
when (loginState) {
is SuccessfulLoginFormState -> binding.authorize.isEnabled = loginState.isDataValid
is FailedLoginFormState -> {
loginState.usernameError?.let { binding.username.error = getString(it) }
loginState.passwordError?.let { binding.password.error = getString(it) }
}
}
})
loginViewModel.loginResult.observe(this, Observer {
val loginResult = it ?: return@Observer
if (loginResult.success) {
showBiometricPromptForEncryption()
}
})
binding.username.doAfterTextChanged {
loginViewModel.onLoginDataChanged(
binding.username.text.toString(),
binding.password.text.toString()
)
}
binding.password.doAfterTextChanged {
loginViewModel.onLoginDataChanged(
binding.username.text.toString(),
binding.password.text.toString()
)
}
binding.password.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE ->
loginViewModel.login(
binding.username.text.toString(),
binding.password.text.toString()
)
}
false
}
binding.authorize.setOnClickListener {
loginViewModel.login(binding.username.text.toString(), binding.password.text.toString())
}
}
Again the only essential difference between LoginActivity
and this code for EnableBiometricLoginActivity
is that showBiometricPromptForEncryption()
is called after the server returns a userToken.
In order to launch EnableBiometricLoginActivity
, we have to add code in the onCreate() function of LoginActivity
to start that.
Finally add the following code snippet to complete the implementation for EnableBiometricLoginActivity
.
private fun showBiometricPromptForEncryption() {
val canAuthenticate = BiometricManager.from(applicationContext).canAuthenticate()
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
val secretKeyName = "biometric_sample_encryption_key"
cryptographyManager = CryptographyManager()
val cipher = cryptographyManager.getInitializedCipherForEncryption(secretKeyName)
val biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(this, ::encryptAndStoreServerToken)
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
private fun encryptAndStoreServerToken(authResult: BiometricPrompt.AuthenticationResult) {
authResult.cryptoObject?.cipher?.apply {
SampleAppUser.fakeToken?.let { token ->
Log.d(TAG, "The token from server is $token")
val encryptedServerTokenWrapper = cryptographyManager.encryptData(token, this)
cryptographyManager.persistCiphertextWrapperToSharedPrefs(
encryptedServerTokenWrapper,
applicationContext,
SHARED_PREFS_FILENAME,
Context.MODE_PRIVATE,
CIPHERTEXT_WRAPPER
)
}
}
finish()
}
At this point, if you run the app, it will look like your work is done. But not quite. You still have to implement showBiometricPromptForDecryption()
inside LoginActivity
so that the user can continue to be able to login with Biometrics going forward.
Add Logic to LoginActivity for Biometrics Authentication
Inside LoginActivity
, replace the showBiometricPromptForDecryption()
placeholder with the following code.
// BIOMETRICS SECTION
private fun showBiometricPromptForDecryption() {
ciphertextWrapper?.let { textWrapper ->
val secretKeyName = getString(R.string.secret_key_name)
val cipher = cryptographyManager.getInitializedCipherForDecryption(
secretKeyName, textWrapper.initializationVector
)
biometricPrompt =
BiometricPromptUtils.createBiometricPrompt(
this,
::decryptServerTokenFromStorage
)
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
private fun decryptServerTokenFromStorage(authResult: BiometricPrompt.AuthenticationResult) {
ciphertextWrapper?.let { textWrapper ->
authResult.cryptoObject?.cipher?.let {
val plaintext =
cryptographyManager.decryptData(textWrapper.ciphertext, it)
SampleAppUser.fakeToken = plaintext
// Now that you have the token, you can query server for everything else
// the only reason we call this fakeToken is because we didn't really get it from
// the server. In your case, you will have gotten it from the server the first time
// and therefore, it's a real token.
updateApp(getString(R.string.already_signedin))
}
}
}
// USERNAME + PASSWORD SECTION
4. Done.
You did it! Congratulations! You've given your users the convenience of biometric authentication! Along the way, you learned the following:
- How to add BiometricPrompt to your app.
- How to create a Cipher for encryption and decryption.
- How to store your server-generated user token for biometric authentication.
For more on how BiometricPrompt and cryptography work together, see:
- Using BiometricPrompt with CryptoObject: How and Why
- Migrating from FingerprintManager to BiometricPrompt
- Show a biometric authentication dialog
May your app prosper!