Building an App With Biometric Login: Real Cost, Real Failure Modes
Biometric login is a key management problem wearing a Face ID prompt. Added properly to an existing app it runs $15,000 to $60,000. As part of a first release, budget $50,000 to $130,000 over 10 to 16 weeks. A full identity platform with a self hosted passkey server, device fleet revocation and a verified recovery path runs $150,000 to $350,000 over 6 to 12 months. The prompt takes a day. Everything behind it is the project.
What this involves once real users touch it
The demo takes an afternoon. Call BiometricPrompt on Android or LAContext.evaluatePolicy on iOS, the sensor says yes, you call your login function. It works on your phone. It ships.
On a payments app we inherited, a user's partner added their own fingerprint to the phone, which Android allows up to five of and iOS allows as one Alternate Appearance for Face ID. The partner opened the app, went straight into the account, and money moved. Nothing was hacked. The postmortem was three lines: if (result.isSuccess) unlock(), the refresh token sitting in SharedPreferences, and a key never bound to the enrolled biometric set. The sensor answered "is this a valid finger on this device," which is not "is this the person who owns this account."
That gap is the discipline. Biometric login is a local gate over a cryptographic key, and the cost lives in how that key is created, invalidated, restored and revoked.
The biometric never proves anything to your server
Your backend cannot see a face. The only thing that crosses the network is a signature produced by a private key the secure hardware refused to release until the sensor matched.
So the correct build generates a key inside the Secure Enclave or the Android Keystore with setUserAuthenticationRequired(true) and hands it to the prompt as a CryptoObject. The server sends a random challenge, the key signs it, the server verifies against the public key registered at enrollment. If the private key never leaves hardware, a rooted device cannot fake the answer.
Skip the CryptoObject and your gate is a boolean, and a boolean is a Frida one liner. Hooking evaluatePolicy to return true is a party trick on jailbroken devices. A two day quote for biometric login is quoting the boolean.
One consequence buyers do not expect: with WebAuthn and passkeys, your server receives a User Verified flag in the assertion, and that flag is true whether the user used their face, their fingerprint or the device PIN. No bit says "Face ID happened." If a product team wants a dashboard of who logged in biometrically, tell them now that the protocol does not carry it. Same with the signature counter: synced platform passkeys from iCloud Keychain return a signCount of zero, so the clone detection FIDO documentation describes does nothing for the authenticators most of your users have.
Key invalidation is what pages you at 3am
Hardware backed keys are deliberately brittle. That is the point, and it is also the outage.
On Android, a key created with setInvalidatedByBiometricEnrollment(true) is destroyed the moment the user adds a fingerprint, and your next call throws KeyPermanentlyInvalidatedException. Catch it and you re-enrol gracefully. Miss it and the app crashes on the login screen for that user forever, because the exception fires on every launch and the only escape is clearing app data.
On iOS the same problem wears two disguises. Keychain items created with .biometryCurrentSet become unreadable when Face ID enrollment changes, while .biometryAny does not, which is the flag that let the partner into the payments app. Separately, LAContext.evaluatedPolicyDomainState is an opaque blob that changes when the enrolled set changes. Storing it at enrollment and comparing on each launch detects "a new face was added since this user trusted this device," which is a risk decision your business should make, not your iOS developer.
Then the restore case. Keychain items marked kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly do not migrate to a new phone. Correct behaviour, and also the incident: the week a major iOS version ships and a chunk of your base migrates devices, all of those users are silently logged out at once. If your fallback is an SMS one time code, you have aimed your entire user base at your Twilio spend cap inside a four hour window.
Two smaller landmines with outsized cost. NSFaceIDUsageDescription missing from Info.plist crashes the app the first time Face ID is invoked, and it does not crash on Touch ID devices, so it survives testing on an older test phone and detonates on release day. And you cannot meaningfully test any of this in CI: the iOS Simulator gives you Features, then Face ID, then Matching Face, the Android emulator takes adb -e emu finger touch 1, and cloud device farms do not drive real sensors. Biometric regression testing is manual on a physical device matrix, permanently. Budget it as a recurring line.
Android is three products sharing one API
The AndroidX BiometricPrompt API presents one clean surface over an uneven fleet, and the seam is the authenticator class.
Only Class 3, exposed as BIOMETRIC_STRONG, can unlock a Keystore key. Class 2, BIOMETRIC_WEAK, cannot back a CryptoObject at all. Plenty of budget and mid range handsets ship camera based face unlock that is Class 2. So a user with face unlock working on their lock screen calls canAuthenticate(BIOMETRIC_STRONG) and gets BIOMETRIC_ERROR_NONE_ENROLLED. Support will file this as a bug. It is the security model. The decision is whether you fall back to DEVICE_CREDENTIAL, the PIN or pattern, which does unlock Keystore keys, or tell that segment biometric login is unavailable.
Then the error codes. ERROR_LOCKOUT is a 30 second cooldown after five failures. ERROR_LOCKOUT_PERMANENT needs a device credential to clear and is a different screen. ERROR_CANCELED from the system is not ERROR_USER_CANCELED, and treating them alike produces the bug where the prompt loops after an incoming call. ERROR_VENDOR is a documented escape hatch for OEM specific codes with vendor defined meanings, which in practice means certain Xiaomi and Huawei builds return values nobody has a table for. iOS is tidier but still needs separate handling for .userFallback, .systemCancel, .biometryNotEnrolled and .biometryLockout. That is roughly a dozen states, each with different copy and a different next action, and it is most of the QA effort on this feature.
Passkeys and the two files that fail silently
If you want passwordless login rather than a convenience gate over an existing session, you want passkeys, which means running a FIDO2 relying party: SimpleWebAuthn on Node, java-webauthn-server from Yubico, or py_webauthn. You store the credential ID, the public key, the AAGUID and the sign counter, and you verify attestation at registration.
The part that eats a sprint is domain association. iOS needs an apple-app-site-association file served from /.well-known/ with a webcredentials entry and your team ID. Android needs assetlinks.json carrying the SHA-256 fingerprint of your signing certificate. With Play App Signing enabled, the fingerprint on your machine is not the one Google serves, so a developer who copies their local fingerprint ships passkeys that work in debug and fail on every production install, with no error a user could report. Verify against the fingerprint in the Play Console, and again after any key rotation.
Recovery is where the money and the fraud go
Every biometric enrollment is bootstrapped from something else, and that something else is your real security level. Ship a flawless Secure Enclave implementation behind an SMS reset and you have built an SMS authentication app with a nice animation. SIM swap crews go straight there.
A serious build treats re-enrollment on a new device as its own risk decision: step up verification, a cooling off window before the new device can move money, a notification to the old device, an audit trail. In EU payments, PSD2 strong customer authentication wants two factors from separate categories plus dynamic linking, meaning the amount and the payee are cryptographically bound into what the user approves. Signing an opaque nonce does not satisfy an auditor. Signing a challenge derived from the transaction details does.
One hard line: do not casually build server side face matching. Storing a face template on your servers puts you inside the Illinois Biometric Information Privacy Act, which requires written consent and a published retention schedule and carries a private right of action. Texas CUBI and Washington have their own regimes. On device Face ID and Touch ID keep you out of this entirely, because Apple and Google never hand you the template. That is a legal reason to stay on platform biometrics, not only an engineering one.
What it costs and how long it takes
These are Digital Heroes delivery bands, drawn from what this capability has taken across 2,000+ projects.
Added to an existing app with working auth, a local biometric gate over a hardware bound refresh token, both platforms, real error handling and a re-enrollment path: $15,000 to $60,000. As part of a first release, where you also build the session model and recovery flow it depends on: $50,000 to $130,000 in 10 to 16 weeks. A full platform with a self hosted FIDO2 relying party, device fleet registry, admin revocation console, verified recovery and regulator evidence: $150,000 to $350,000 over 6 to 12 months.
What moves this capability's number. Web in scope pushes you from a local gate to a real relying party, roughly doubling the backend. Regulated payments or health adds dynamic linking, audit artefacts and a documented threat model. Offline authentication, common in field and logistics apps, removes the server challenge and forces a device local trust model that is hard to get right. Enterprise deployment adds work profiles and MDM behaviour. And the device matrix: supporting Class 2 only Android hardware is an extra branch of design and QA, and it is the most commonly forgotten item in other people's quotes.
When you should not build this
If you are not an identity provider for anybody else, do not write your own FIDO2 relying party. Take a managed one: Auth0, Stytch, Descope, Hanko or Corbado. They price per monthly active user, a rounding error at 10,000 users and a real line item at 500,000, and by the time it is a real line item you will know enough to bring it in house on your terms.
The buyer who should take that deal: a consumer or B2B SaaS product where auth is a cost centre, with no offline requirement and no data residency mandate. Buy the passkey server, keep the client work, because that is where your differentiation and all of the failure modes live. Building it yourself is right in three cases: authentication is your product, you are contractually forbidden from third party identity storage, or you must authenticate with no network. Outside those three, a custom relying party is a hobby you are paying for.
How to brief and vet a developer
Give them the constraints that decide the architecture: regulated or not, offline or not, web in scope or not, whether you own the existing session model, and your worst supported Android device.
Then ask these. What happens when a user adds a fingerprint after enrolling, and where exactly does that exception get caught? Show me where the key is created, and prove the biometric result is not a boolean. What is the difference between Class 3 and Class 2 on Android, and what do users on Class 2 hardware see? Which Keychain access control flag are you using, and why that one? What is our recovery flow, and what stops a SIM swap from owning it? How do we revoke one device without logging out the fleet? Who verifies our assetlinks fingerprint against Play App Signing?
Someone who has shipped this answers the enrollment question in one sentence, usually with a war story attached. Someone who has not will describe the prompt.
The evidence behind this guide
Independent findings on why this investment pays off. Every link goes to the primary source.
- Standish's 2015 CHAOS research found roughly a third of software projects (about 36% by the Modern definition) fully succeed on time, on budget, and on scope, with top success drivers including executive support, user involvement, and clear requirements/business objectives. Source: Standish Group (CHAOS Report) (2015) →
- Brands not sending push notifications can lift 90-day app retention by 190%, and forfeit roughly 95 cents of every dollar spent on user acquisition when opted-in users receive no messages within 90 days; rich notifications with images see 56% higher direct open rates. Source: Airship (2024) →
- Only about 30% of digital transformations succeed at meeting their objectives, but getting six critical success factors in place (leadership commitment, talent, agile culture, progress monitoring, clear strategy, and a modernized platform) raises the odds of success from 30% to 80%. Source: Boston Consulting Group (BCG) (2020) →
- Companies in the top quartile of McKinsey's Developer Velocity Index had 2014-18 revenue growth four to five times faster than bottom-quartile peers, showing that software-building capability is a driver of business performance, not just a support function. Source: McKinsey & Company (2020) →
Rohan advises mid-market and enterprise teams on ERP, CRM and custom software, and has led delivery on dozens of business-software builds.
Writes for Digital Heroes, shipping business software for 2,000+ brands across 55+ countries since 2017.