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
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 is a data class that you create so that you can conveniently store the encrypted
ciphertext, in persistent storage such as
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
CryptoObject. Finally you pass the
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
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
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:
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
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.
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.