Implement Android biometric attributes as narrowly scoped authentication context, not as a fingerprint of the person. Decide which authenticators each action accepts, check availability, and for anything a server must trust, unlock a Keystore key with a Class 3 (BIOMETRIC_STRONG) biometric through a CryptoObject. Verify that key once with key attestation when you register it, then have it sign a fresh, operation-specific challenge your server checks. A local success callback is never enough for a server to authorise a transaction. Test cancellation, missing hardware, changed enrolment and recovery alongside success. This guide covers Android only.
Define what the biometric result proves
Android grades authenticators. Class 3 (BIOMETRIC_STRONG) biometrics meet the highest security bar and can unlock Keystore keys. Class 2 (BIOMETRIC_WEAK) biometrics can confirm presence but cannot be used with a CryptoObject. DEVICE_CREDENTIAL is the screen-lock PIN, pattern or password. The app declares which of these it accepts with setAllowedAuthenticators() and checks them with BiometricManager.canAuthenticate().
The app never receives biometric data. Android performs the match in its secure environment and returns only a result.
Establish the Android scope
- List the device models, Android releases, package variants and signing channels you serve. Record the
androidx.biometricversion and any framework bridge. - Name the protected action: reopening a local screen, decrypting local data or approving a server transaction. Each needs a different level of proof.
- Choose the accepted authenticators for that action. Some combinations, such as
BIOMETRIC_STRONG | DEVICE_CREDENTIAL, are not supported on older releases, so callcanAuthenticate()with the exact combination you will pass to the prompt. - Keep availability, prompt result, key operation and server decision as separate entries in your test record. An unavailable sensor is not a failed authentication.
| Observation | What it means | What it does not mean |
|---|---|---|
canAuthenticate() returns success | The device can attempt the chosen authenticators | Anyone has authenticated |
| Prompt reports success | Local authentication completed under the chosen policy | The server received a fresh approval |
getAuthenticationType() reports biometric or device credential | Which route the user took | That the route meets the action’s policy |
A CryptoObject key operation succeeds | A Class 3 biometric unlocked that key for this operation | A legal identity or an unmodified app |
| Server accepts the signed challenge | Your verifier accepted this request and credential | Approval of any other operation |
Otukan protects Android biometric flows against runtime manipulation, including hooking of the biometric callback. It ties approval to a Keystore key that the platform unlocks only after a successful match. For product scope, see biometric flow protection.
Register the key and verify fresh requests
Create and attest the key
Generate an asymmetric key pair in the Android Keystore with KeyGenParameterSpec:
setUserAuthenticationRequired(true)and, on Android 11 or higher,setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG), so every use needs a fresh Class 3 authentication.setInvalidatedByBiometricEnrollment(true), so adding a new fingerprint or face makes the key unusable.setAttestationChallenge()with a one-time value from your server.setIsStrongBoxBacked(true)where the device has StrongBox and your policy wants it.
Send the public key and its attestation certificate chain to the server. Key attestation is how the server knows the key really lives in secure hardware and is bound to biometric use. The server should:
- Verify the chain up to a Google attestation root, and check each certificate against Google’s revocation status list.
- Confirm the attestation challenge matches the value it issued.
- Read the key description extension: the attestation security level (
TrustedEnvironmentorStrongBox), the user-authentication requirements and the root of trust, including verified boot state. - Check the attestation application ID: your package name and the digest of your signing certificate.
- Store the public key against the account and device registration, with a way to revoke it.
Register only after the account has authenticated through your approved enrolment flow.
Approve an operation
- Issue a challenge. The backend creates an unpredictable, single-use challenge tied to the session, the action and its details, with a short expiry.
- Check availability. Call
canAuthenticate(BIOMETRIC_STRONG)immediately before prompting. - Unlock the key. Initialise a
Signaturewith the private key, wrap it in aBiometricPrompt.CryptoObjectand pass it toauthenticate(). - Sign. In
onAuthenticationSucceeded(), use theSignaturereturned in the result to sign the challenge and the action details. Never substitute a success flag for this signature. - Verify and consume atomically. The backend checks the signature against the registered key, the challenge, the action binding, the expiry and the account state before applying the transaction, and rejects a second use of the same challenge.
With Otukan, your server verifies a signature over a one-time challenge rather than a success flag.
Test both sides of the boundary
Run a legitimate approval first and keep a redacted record of the challenge, the local outcome and the server decision. Then replay the signed challenge, change the amount or payee, switch the account and submit after expiry. Each should be rejected without locking the legitimate user out.
In an authorised lab, tamper with the app’s callback handling. Record whether the protected flow raises the expected signal, and confirm the backend still rejects a request without a valid signature.
Repeat with cancellation, lockout, backgrounding, concurrent prompts and network loss after local approval. Resolve an uncertain transaction by checking server state before retrying; never ask the user to approve a second transaction because the first response was lost.
Minimise data and manage credential changes
Keep outcome categories and the minimum correlation needed for support. Restrict access to the credential registry and set separate retention periods for security evidence and operational logs.
When a user enrols a new biometric, an invalidated key throws KeyPermanentlyInvalidatedException. Treat that as a credential lifecycle event, not as fraud. Otukan supports keys that stop working on new enrolment, with a step-up you define to re-establish trust. Recovery should go through your account verification path before a new key is registered.
- Reinstall and reset do not restore authority from a stale local flag.
- New enrolment, removed enrolment and screen-lock changes lead to the documented recovery.
- Rotation retires old keys under a deliberate transition policy.
- Revocation blocks further approvals even if a local session remains open.
- Users without suitable hardware, or with accessibility needs, have another route.
- Logs contain no reusable signature, challenge or session secret.
For every run, record device, OS, app build, signing channel, configuration, expected outcome, observed outcome and reviewer. Keep clean-device controls alongside adverse cases. Wider platform context is covered on Android app security, and the iOS implementation is covered on the iOS biometric attributes guide.
Further reading
- Show a biometric authentication dialog — Android Developers
- Android Keystore system — Android Developers
- Verify hardware-backed key pairs with key attestation — Android Developers
- BiometricManager.Authenticators — Android API reference
- Measuring biometric unlock security — Android Open Source Project