Biometric Authentication on Android — Part 2
Critical User Journeys and UI
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 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)
.
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.
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 Fragment
s (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.
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!