Biometric Authentication on Android — Part 2

Critical User Journeys and UI

Isai Damier
Android Developers

--

This is Part 2 of the Biometric Authentication on Android series. For more information, see Part 1.

To augment your login process to include biometric authentication, prompt the user to enable biometric authentication right after they successfully sign in. Figure 1A shows a typical sign-in flow, which you are probably already familiar with. After the user presses the sign-in button and a userToken is returned from the server, then prompt them to enable biometrics, as shown in figure 1B. Once the user has enabled biometric authentication, from then on the app should automatically present a biometric prompt each time the user needs to sign in, as shown in figure 2.

Figure 1A: Typical sign-in UI
Figure 1B: Enable biometric authentication
Figure 2: Confirm biometric authentication

Figure 2: Confirm biometric authentication

While figure 2 shows a confirmation button in the UI, that button is actually optional. For instance, if you are building a restaurant app, it’s recommended that you show this confirmation button because the biometric authentication allows patrons to pay for their meals. For sensitive transactions and payments, we recommend that you require confirmation. To include a confirmation button in your app’s UI, call setConfirmationRequired(true) while building your BiometricPrompt.PromptInfo. Note that if you don’t call setConfirmationRequired() the system will set it to true by default.

Biometrics Design Flow

By way of example, the code snippets use the encrypted version of BiometricPrompt, with CryptoObject.

If your app requires authentication, you should create a dedicated LoginActivity component that serves as your app’s landing page. This is important no matter how frequently you require your users to authenticate, so long as authentication is required. If the user is already authenticated, then that LoginActivity will call finish(), and the user can just move along. If the user is not yet authenticated, then you should check whether biometrics has been enabled.

There are a few ways to check whether biometrics has been enabled. However, instead of wading through the diverse alternatives, let’s dive into one in particular: a null check on the custom property ciphertextWrapper. The CiphertextWrapper is a data class that you create so that you can conveniently store the encrypted userToken, a.k.a ciphertext, in persistent storage such as SharedPreferences or Room when the user successfully enables biometric authentication for your app. Therefore, if ciphertextWrapper is not null, then you have the encrypted version of the userToken that’s needed to access the remote server — which is to say biometrics is enabled.

If biometrics is not enabled, then the user may click to enable it (figure 1B), at which point you would show the user the actual biometric prompt, as shown in figure 3.

In the following snippet, showBiometricPromptForEncryption() shows how to set up a cryptographic key that you associate with the biometric prompt. Essentially, you initialize a Cipher from a String, and then pass the Cipher to CryptoObject. Finally you pass the CryptoObject to biometricPrompt.authenticate(promptInfo, cryptoObject).

Figure 3: Prompt to enable biometrics

At this point, i.e. figure 2 and figure 3, the app only has the userToken. But unless the user will keep using their password every time they open the app, that userToken needs to be stored in the app’s filesystem for later sessions. However, if you store the userToken without first encrypting it, then an attacker with unauthorized access to the device can potentially read the userToken and use it to grab data from the remote server. Hence, it’s good to encrypt the userToken before saving it locally. This is where the biometric prompt in figure 3 comes into play. As the user authenticates with their biometrics, your objective is to use BiometricPrompt to unlock a secret key (either auth-per-use or time-bound) and then use that key to encrypt the server generated userToken before storing it locally. From now on, when the user needs to sign in, they can use their biometrics to authenticate (i.e. biometric authentication -> unlock secret key -> decrypt userToken for server access).

Note the distinction between when the user first enabled biometrics and all the subsequent times when the user is actually signing-in using biometrics. To enable biometric authentication, the app calls showBiometricPromptForEncryption(), which initializes a Cipher for encrypting the userToken. To actually sign-in with biometrics, on the other hand, the app calls showBiometricPromptForDecryption(), which initializes a Cipher for decryption and then uses the Cipher to decrypt the userToken.

Having enabled biometric authentication, the user should see the biometric prompt to authenticate next time they return to the app, as shown in figure 4. Notice that because figure 4 is for signing into the app — as opposed to figure 2 which is for transaction confirmation — no confirmation is necessary since app login is a passive, easily-reversible action.

Figure 4

To implement this flow for your user, when your LoginActivity verifies that the user has already authenticated, you would use the cryptographic object, unlocked by successful BiometricPrompt authentication, to decrypt the userToken and then call finish() on the LoginActivity component.

The Complete Picture

Figure 5 shows a complete flow diagram of the recommended engineering design. We are fully aware that your code may deviate from this recommendation in a number of places. For instance, your own cryptographic solution may require unlocking the key for encryption only and not for decryption. Still we provide a complete sample solution for those who may need it.

Wherever the diagram mentions secret key, you are free to use an auth-per-use key or a time-bound key. Also wherever the diagram mentions “the app’s memory,” you are free to use your favorite solution for structured data storage: SharedPreferences, Room, or anything else. Finally a userToken, as it’s typically called, is any sort of server payload that would give a user access to restricted data or services. The server would normally check for the presence of such payload as evidence that the caller is authorized.

In the diagram, the arrow from “encrypt userToken” could very well go to “login completed” instead of back to “LoginActivity”. Nonetheless, we choose “LoginActivity” to call attention to the fact that it’s okay to use an additional Activity, e.g. EnableBiometricAuthActivity, after the user clicks to “enable biometrics”. Using a separate activity may make your code more modular and therefore more readable. Alternatively, you can create a LoginActivity with two Fragments (backed by a navigation component): one Fragment for the actual authentication flow and one Fragment that responds to the user clicking “enable biometric”.

In addition to this engineering flow diagram, we have published a design guideline that you can follow when implementing your app. Also, our sample on Github should provide you with further insight.

Figure 5: Complete diagram of biometric login with remote server

Part 2 Summary

In this post, you learned the following:

  • Which UI assets to use to augment your login process with biometrics.
  • The critical user journeys your app should address for biometric authentication.
  • How to design your code to handle the different aspects of biometric authentication.
  • A complete engineering picture of how your login system should flow.

Happy coding!

--

--

Isai Damier
Android Developers

Android Engineer @ Google; founded geekviewpoint.com; Haitian; enjoy classical lit and chess. Twitter: @isaidamier