Login with Biometrics on Android

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.

de4ebec8e6714a8f.png

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.

  1. Start with an app that has a typical login Activity (provided for you).
  2. Add a button that gives users the option to "use biometric" authentication.
  3. Create a biometric authorization Activity to associate a server-generated user token with the user's biometric credentials.
  4. 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.

f9910772d5fd481a.png

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.

  1. First we will set up a CryptographyManager class to handle encryption, decryption and storage of the user token.
  2. Then, since both LoginActivity and EnableBiometricLoginActivity need to call BiometricPrompt, we will create a BiometricPromptUtils file for the shared code.
  3. 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.

1aa54bc48a4a082d.png

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.

2969f8d59441fc1d.png

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.

  1. The username and password TextViews inside EnableBiometricLoginActivity should be wired similarly to those inside of LoginActivity.
  2. Unlike LoginActivity, however, when the user clicks the "Authorize" button, you will launch the BiometricPrompt.
  3. When the BiometricPrompt returns, you will use the associated Cipher to encrypt the server-generated user token.
  4. 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:

May your app prosper!