Considerations and Approaches for Implementing Biometric Login: Part 2
In Part 1, we discussed how we designed our biometric authentication system with both privacy and a better user experience in mind. In this post, we'll show you exactly how we did it.
To build a new authentication mechanism, we opted to use the react-native-biometrics library. This library interfaces with the device's biometrics, generates a keypair, and stores the private key within the device's keystore. When logging in with biometrics, the library creates a signature using the stored private key, and the signature is then sent to the server for validation. (Note: this library is now unmaintained. At the time of writing, this implementation can instead be accomplished with the following maintained libraries: expo-local-authentication to access device biometrics, keypair to generate a keypair, react-native-keychain to access the device keystore.)
The implementation flow is as follows:
Check if biometrics are supported and configured by the user for this device.
const { available } = await ReactNativeBiometrics.isSensorAvailable();
If yes, prompt the user for a biometric scan.
const { success } = await ReactNativeBiometrics.simplePrompt({ promptMessage: 'Confirm ID' });
If the scan succeeds, generate a keypair. The react-native-biometrics library's createKeys method will automatically save the private key to the device's keystore, and return the public key counterpart. Send the public key to your server.
const { publicKey } = ReactNativeBiometrics.createKeys('Confirm biometrics');
// Save the public key to your database
post('/save_biometric_key', { key: publicKey, device_id: 'xyz' })
When the user tries to login with biometrics, scan their biometrics. This time, if the scan succeeds, create a signature using the stored private key. The react-native-biometrics library's createSignature method handles all of this.
// Prompts for scan
// On scan success, accesses private key and uses it to generate a signature, which is then returned
const signatureResponse = ReactNativeBiometrics.createSignature({
promptMessage: 'Face or fingerprint ID for Alto Pharmacy',
payload: BIOMETRIC_SIGNATURE_PAYLOAD,
});
Send the signature to the server to validate against the public key. If validation succeeds, the user has successfully authenticated with biometrics and can proceed into the app.
// Call your server API to validate the signature against the public key that you've stored in the database
const validationSuccess = await post(
`/validate_biometric_signature`,
{
signature: signatureResponse.signature,
payload: BIOMETRIC_SIGNATURE_PAYLOAD,
device_id: 'xyz',
},
);
if (validationSuccess) {
// Login Success
} else {
// Login failure
}
Putting the whole flow together:
import ReactNativeBiometrics from 'react-native-biometrics';
const setUpBiometricLogin = async () => {
// Check if biometrics are supported and configured on this device
const { available } = await ReactNativeBiometrics.isSensorAvailable();
if (available) {
// Prompt user to scan biometrics
const { success } = await ReactNativeBiometrics.simplePrompt({ promptMessage: 'Confirm ID' });
if (success) {
// If scan is successful, generate a keypair, then save the public key to the server
const { publicKey } = await ReactNativeBiometrics.createKeys('Confirm biometrics');
post('/save_biometric_key', { key: publicKey, device_id: 'xyz' })
}
}
}
const loginWithBiometrics = async () => {
// Scan user's biometrics. On scan success, generate a signature
const signatureResponse = await ReactNativeBiometrics.createSignature({
promptMessage: 'Face or fingerprint ID for Alto Pharmacy',
payload: BIOMETRIC_SIGNATURE_PAYLOAD,
});
// Validate the signature against the public key saved on the server
const validationSuccess = await post(
`/validate_biometric_signature`,
{
signature: signatureResponse.signature,
payload: BIOMETRIC_SIGNATURE_PAYLOAD,
device_id: 'xyz',
},
);
if (validationSuccess) {
// Login Success
} else {
// Login failure
}
}
On the server side, you'll need to:
1. build a way to store a public key associated with a device
2. write a method to validate a signature against a public key
3. expose endpoints to save a public key, and validate a signature
Once biometric authentication is built, there are additional decisions to make. The right decisions for your app will depend on your goals; the approach we took here at Alto may not align with what is best for your app.
How should biometric sessions interact with our existing email and password sessions?
We opted to treat email and password sessions as longer-lived primary sessions, with biometrics as a shorter-lived secondary session on top of that primary session. This means that a user must first login with email and password in order to use biometrics on subsequent app opens. If the user logs out for any reason, they will need to log in again with email and password before they can use biometrics again.
How often should we prompt the user for biometrics vs. email and password?
We opted to prompt the user for biometrics far more frequently than email and password, but made this configurable in case we want to adjust.
How do we keep track of session expiry?
We use tokens and cookies for session management.
How do we handle edge cases?
There are plenty of edge cases to consider as you're developing biometric capabilities for your app, including: what should happen when the biometric scan fails, or when the signature validation fails? What should happen if a user disables biometric permissions from their device? What should happen when two users share a device? What should happen if 'risky' activity is flagged for this account? How can we disable biometric login if needed?
Conclusion
Face and fingerprint ID login has clear benefits for both the end-user and the application itself, but it comes with a host of implementation decisions. If you're considering building biometric login for your app, I hope this article gives you clarity on what biometrics is, the different ways you can integrate it into your app, and the tradeoffs and edge cases to consider.
At Alto, the Patients Engineering team worked closely with our Security Engineering and Product teams to align on our goals and build the best solution for our needs. Our Security team offered deep expertise on the authentication strategies to adopt and avoid, and our Product team kept us tethered to building the best experience for our users.
Working as a team is one of the best parts about developing at Alto. If you want to meet the awesome team I'm talking about, come join us - we're hiring!