Biometric authentication looks like a solved problem because both platforms ship a prompt you can call in a few lines. The prompt is the easy part. The mistakes happen in what the app does with the result, and the most common one turns a hardware-backed control into a decorative one.
Short answer
The frequent error is treating a biometric check as a boolean. An app calls the prompt, receives success, and then unwraps a credential it was holding anyway. Nothing about that flow required the hardware. Android's biometric authentication guidance and Apple's Local Authentication framework both support binding the result to a key that only exists on successful authentication, and that binding is the difference between a real control and a UI gesture.
Mistake one: using the result as a boolean
The broken pattern is straightforward. The app stores a session token, calls the biometric prompt, and on a success callback reads the token and proceeds.
The reason this fails is that the callback is code running in your process, and an attacker with control of the device can reach the token without ever invoking the prompt. The check is enforced by your control flow rather than by the hardware, and control flow is patchable.
The correct pattern binds the secret to authentication. On Android, that means generating a key in the Keystore that requires user authentication and using BiometricPrompt with a crypto object so that the cipher only becomes usable after a successful authentication. On iOS, it means storing the item in the keychain with an access control that requires biometry, so that Keychain services will not return the item without it.
In both cases the platform, not your if statement, is what refuses to hand over the data.
Mistake two: not deciding what happens when biometry changes
Both platforms can invalidate keys when the enrolled biometric set changes, and the choice of whether to opt into that has real consequences.
If your key survives the enrollment of a new fingerprint or face, then anyone who can add their own biometric to the device inherits access to whatever that key protects. On a device where the attacker knows the passcode, that is a straightforward path in.
If your key is invalidated on enrollment change, users who legitimately add a fingerprint get logged out and have to re-authenticate with your primary factor. That is a support cost and a deliberate one.
The failure is not choosing either way. Apple's LAPolicy documentation distinguishes policies that allow a passcode fallback from those that require biometry, and the equivalent Android decision lives in the key generation parameters. Pick according to what the credential protects, and write down why.
Mistake three: confusing device authentication with user identity
A biometric prompt proves that someone who can authenticate to this device is present. It does not prove which of your users that is.
For a single-user consumer app on a personal device, the distinction rarely matters. For anything where accounts are shared, transferred, or where a device may be handed to a family member, it matters a great deal, and treating a biometric success as proof of identity produces audit trails that are simply wrong.
The MASVS authentication category frames this correctly: local authentication is a convenience layer over a session established by real authentication against your backend. The server should be issuing and validating the session; the biometric check is about protecting local access to it.
The practical rule is that a biometric prompt should never be the thing that creates a session. It should be the thing that unseals one your server already issued.
Mistake four: no usable fallback
Biometry fails routinely and for boring reasons: wet hands, a mask, a cracked sensor, a user who never enrolled, a device that does not have the hardware.
An app with no fallback path is unusable for those people, and an app whose fallback is weaker than the biometric path has simply moved the attack. A four digit app-specific PIN behind a fingerprint prompt means the real strength of the control is four digits.
The defensible design is a fallback to the device credential where the platform supports it, or to your primary account authentication, rather than to a secondary secret you invented. The device passcode is enforced by hardware with rate limiting; a PIN you check yourself is not.
Also handle the states that are neither success nor failure. Hardware unavailable, no enrolled biometrics, permanently locked out after repeated attempts: each needs a distinct message, because a generic error here produces users who believe the app is broken.
Mistake five: putting the wrong things behind it
Biometry is worth using for local access to a session, a stored credential, a payment confirmation or a sensitive view. It is not a substitute for authorization on the server.
An app that shows or hides functionality based on a local biometric check is enforcing policy on a device the user controls. If the underlying API will serve the data to any authenticated session, the biometric gate is cosmetic.
The test to apply is whether the same request, made directly against your API with a valid token, returns the same data. If it does, the biometric prompt is a UI affordance rather than a control, and it should be described that way internally so nobody builds on a false assumption.
Checking it in a built app
These mistakes are visible in a built artifact more reliably than in a code review, because what matters is which platform APIs were actually called and how the keys were created.
The things to look for are a keychain item stored without an access control that requires biometry, a Keystore key generated without requiring user authentication, a biometric callback that reads a secret the app was already holding, and a fallback PIN checked in application code.
Manual inspection is documented in the OWASP MASTG, and the storage side of the question is covered by the MASVS storage category. For teams shipping frequently, an automated pass over the release artifact such as PTKD.com will flag the storage and configuration half of this: credentials sitting in unprotected storage, keychain items with permissive accessibility, keys created without authentication requirements.
The design half still needs a person, because whether a biometric result gates a real key or an if statement is a question about intent.
What the platforms actually promise
Being precise about the guarantee helps, because a lot of biometric design rests on assumptions the platforms never made.
Neither platform gives your app the biometric data. You receive an authentication result, and on the correct integration you receive access to a key. The template stays inside hardware you cannot query, which is why "we store fingerprints" is never an accurate description of a mobile app and should not appear in your privacy policy.
Neither platform promises that biometry is stronger than the device passcode. Rate limiting and lockout behaviour are what make both practical, and a device with no passcode set has no secure enclave-backed protection to fall back on.
Both platforms distinguish classes of biometric strength. Android exposes authenticator types with different strength classes, and only the stronger class can be used to release cryptographic keys. Requesting the weaker class because it produced fewer support tickets is a decision to accept a weaker control, and it should be made knowingly.
Common failure states and what to say
Users interpret authentication errors as app defects, so the messages matter more than they seem.
No biometrics enrolled. The user has the hardware and has not set it up. The correct response offers to continue with the device credential and, on platforms that allow it, a link to the relevant settings.
Hardware unavailable. Temporarily inaccessible, often because another process holds it. Retry is reasonable here, and a permanent failure message is not.
Lockout after repeated failures. The platform has disabled biometry for a period. The message needs to say so explicitly, because a user seeing a generic failure will assume their enrolled fingerprint stopped working.
Permanent lockout. Requires the device credential to clear. Your fallback path is the only route through, which is another reason it needs to exist.
Cancelled by the user. Not an error. Treat it as a decision and return them to a state that does not immediately prompt again, since a prompt loop is the fastest way to get an app deleted.
Testing biometric flows properly
Simulators make this easy to get wrong, because the enrollment states you need to test are the ones nobody sets up by hand.
Test with biometry enrolled, with it not enrolled, and with the hardware disabled. Test the enrollment-change path by adding a biometric between two runs, which is the only way to see whether your key invalidation behaves as designed.
Test the fallback on its own, without a working biometric, because that path frequently receives less attention and is the one every user with a cracked sensor lives in.
Test on a device with no passcode set, which is rarer than it used to be and still exists. Hardware-backed key protection depends on the device credential, so an app that assumes a secure enclave is available needs a defined behaviour when it is not, rather than an unhandled error at the point of key generation.
And test what happens after a restore to a new device. Keys bound to hardware do not migrate, which means a restored user must re-authenticate with your primary factor. Apps that assume the local credential survives a device transfer produce a confusing dead end at exactly the moment a user is least patient.
Designing the flow around a real session
The design that avoids all five mistakes has a shape worth stating plainly, because it is short.
The user authenticates against your backend with a real factor. The backend issues a refresh credential. The app stores that credential in hardware-backed storage with an access control requiring authentication, and holds nothing else that would grant access.
On subsequent launches, the app requests the credential. The platform prompts, and on success releases it. The app exchanges it for a short-lived access token. On failure, the app has nothing usable and falls back to full authentication.
Two properties fall out of that shape. There is no moment where a boolean decides access, and there is no long-lived token sitting in storage that a successful prompt merely reveals.
It also degrades correctly. A device with no biometrics enrolled still works through the device credential path. A restored device has no key, so the user re-authenticates, which is the right outcome rather than a bug.
The remaining design decision is how long the credential you unseal stays valid. A refresh credential that never expires converts a one-time device compromise into permanent access, so it needs a server-side lifetime and a way to revoke it. Pair the local control with a session list the user can see and terminate, because that is the only mechanism that helps someone who has lost a device.
What to take away
Bind the secret to the authentication rather than checking a boolean, using a crypto object on Android and a biometry-backed access control on iOS.
Decide explicitly what happens when the enrolled biometric set changes, and record the reasoning.
Treat a biometric success as unsealing a session your server issued, never as establishing identity.
Provide a fallback that is at least as strong as the biometric path, which usually means the device credential rather than an app PIN.
And confirm that whatever the prompt protects is also protected server-side, because a control the user's device enforces is a control the user's device can be made to skip.
None of this makes biometry a weak choice. Used correctly, a key that the platform refuses to release without authentication is one of the stronger controls available on a mobile device, and considerably better than a password typed into a phone. The failure is not the technology, it is wiring it to a conditional statement and calling the job done.



