How to Add Face ID and Fingerprint Login to a Mobile App
Practice4 minInnovialabs editorial team
In short
Biometrics never hand your app a fingerprint or face; they only unlock a key or token in the device's secure storage, so proper Face ID login is built on Keychain/Keystore, not on a yes/no check.
How it actually works
Your app never receives a fingerprint or a face scan. Matching happens in a secure module on the device: the Secure Enclave on iPhone, a TEE or dedicated chip on Android. The system only tells the app the result.
There are two ways to use that result:
- A yes/no check. The app asks "did the user confirm?" and gets
true. Simple, but on a compromised device the answer can be faked. - Cryptographic binding. A secret (such as a refresh token) or an encryption key lives in Keychain/Keystore with a rule "release only after biometrics". Without a successful check the data is simply unavailable.
Use the second approach for signing in. The first is fine for low-stakes things: hiding screen content, confirming a harmless action.
A typical login flow
- The user signs in the normal way the first time: phone and code, password, or Apple/Google.
- The app offers to enable Face ID or fingerprint login.
- The refresh token is saved to secure storage with a biometric requirement.
- On the next launch the system shows the biometric prompt, storage releases the token, and the app gets a fresh session from the server.
The server knows nothing about biometrics; it sees an ordinary token refresh.
Implementation on iOS
Use the Keychain with the .biometryCurrentSet access flag, and add NSFaceIDUsageDescription to Info.plist with a clear explanation; without it the Face ID prompt will not work.
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
.biometryCurrentSet,
nil
)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "refresh_token",
kSecValueData as String: tokenData,
kSecAttrAccessControl as String: access
]
SecItemAdd(query as CFDictionary, nil)
When the app reads this item, the system shows Face ID or Touch ID on its own.
Implementation on Android
Create a key in the Android Keystore that requires user authentication, and pass a Cipher to BiometricPrompt through a CryptoObject. The token can be decrypted only after a successful check.
val spec = KeyGenParameterSpec.Builder(
"session_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
For cryptographic binding, request the BIOMETRIC_STRONG class; weaker unlock methods do not support it.
Flutter and React Native
- Flutter: the
local_authpackage gives you only a yes/no check. To bind to storage, useflutter_secure_storagewith biometric access options or specialised packages that work through Keystore/Keychain. - React Native:
expo-local-authenticationis also yes/no. To keep a token behind biometrics, usereact-native-keychainwithaccessControl, orreact-native-biometrics, which creates keys and signs data.
Before picking a package, check that it really uses Keychain/Keystore under the hood rather than just showing the system dialog.
Fallback and re-enrollment
- Fallback. Biometrics can fail: wet fingers, a mask, lockout after several failed attempts. Always keep a path through the device passcode, an in-app PIN or a full login.
- New fingerprint or face. With
.biometryCurrentSetandsetInvalidatedByBiometricEnrollment(true), adding new biometrics makes the stored secret unavailable; on Android you getKeyPermanentlyInvalidatedException. This is protection, not a bug: handle it, delete the old secret, ask for a full login and let the user re-enable biometrics. - Biometrics turned off. If the user removed all fingerprints or disabled Face ID, go straight to the normal login.
What not to rely on it for
- Biometrics confirm "someone enrolled", not a specific person. A family member's fingerprint may be on the device.
- It does not replace the first login and is not an account recovery method.
- For high-stakes actions (transfers, changing the phone number), add server-side verification: a signature with a key from secure storage or a fresh code.
- A yes/no check without storage is easy to bypass on rooted and jailbroken devices.