On iOS, a biometric check only means something to your server when it unlocks a key. Choose the policy deliberately: .deviceOwnerAuthenticationWithBiometrics accepts Face ID or Touch ID only, while .deviceOwnerAuthentication also accepts the device passcode. For sensitive actions, create a Secure Enclave key protected by biometryCurrentSet, register its public key with your server, and sign a fresh server challenge bound to the action. The server verifies the signature, not a success flag. Detect enrolment changes with the domain state and re-establish trust through a strong path. This guide covers iOS.
For the product control, see biometric flow protection. The Android procedure is covered on Android biometric attributes.
What LocalAuthentication tells you
The LocalAuthentication framework runs the match inside the operating system. Your app learns whether the selected policy succeeded and which biometry the device offers. It never receives face or fingerprint data.
| Value | What it means | What it does not mean |
|---|---|---|
biometryType (.faceID, .touchID, .opticID, .none) | Which biometric interface the device offers | That the user has enrolled or authenticated |
canEvaluatePolicy returns true | The chosen policy can run now | That anyone has authenticated |
evaluatePolicy succeeds | The local policy was satisfied | That your server should approve the action |
LAError such as .userCancel, .biometryLockout, .biometryNotEnrolled | Why this attempt did not complete | That the user or device is malicious |
Read biometryType only after calling canEvaluatePolicy; before that it is not populated. Add NSFaceIDUsageDescription to Info.plist with a clear purpose, or Face ID requests fail.
Choose the policy
.deviceOwnerAuthenticationWithBiometricsrequires Face ID or Touch ID. After repeated failures biometry locks out, and the user must enter the passcode before biometrics work again..deviceOwnerAuthenticationaccepts biometrics or the device passcode. Use it where knowledge of the passcode is acceptable for the action.
Record the policy per action. A transfer approval and a balance view do not need the same rule. Label passcode approvals as passcode approvals in your logs.
The key-based pattern
evaluatePolicy returns a Boolean in your process. Anyone who can hook the callback can turn a failure into a success, and the server cannot tell. Bind the biometric check to a key instead.
Create the key at enrolment
- Create an access control with
SecAccessControlCreateWithFlags, protection classkSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyand flags.privateKeyUsageand.biometryCurrentSet. - Create a P-256 key in the Secure Enclave with
SecKeyCreateRandomKeyandkSecAttrTokenIDSecureEnclave, or with CryptoKit’sSecureEnclave.P256.Signing.PrivateKeyand the same access control. The private key never leaves the Secure Enclave. - Send the public key to your server after the user has authenticated strongly, for example with their password and a second factor. Store it against the account and this app installation.
Sign a challenge for each sensitive action
- The app asks the server to start an action, such as a payment of a given amount to a given payee.
- The server returns a one-time challenge bound to the session, the account and the action details, with a short expiry.
- The app signs the challenge and action details with the private key using
SecKeyCreateSignature. The system shows the biometric prompt because the key requires it. Pass anLAContextthroughkSecUseAuthenticationContextif you want to control the prompt text. - The server verifies the signature with the registered public key, checks the challenge is unused and unexpired, checks the action matches, and consumes the challenge in the same transaction as the decision.
Show the user the same action details that the server binds into the challenge. Secure Enclave keys have no per-key attestation comparable to Android key attestation. If you need evidence that registration came from your genuine app, add an App Attest assertion to the registration request.
Otukan protects the iOS biometric authentication flow against runtime manipulation and detects hooking of the biometric callback. Its runtime threat signals sit beside the signature as separate evidence for your response decision. Otukan does not access or store biometric data; matching stays with iOS.
Detect enrolment changes
With biometryCurrentSet, adding or removing a fingerprint or re-enrolling a face invalidates the key. Signing then fails, and the app must handle that as a defined state, not a crash. Use .biometryAny only if you accept that a newly enrolled biometric can approve actions.
You can also detect the change before signing. Store the current domain state after a successful evaluation and compare it next time. From iOS 18 use LAContext.domainState and its biometry.stateHash; on earlier releases use evaluatedPolicyDomainState, which Apple has deprecated. A changed value means the enrolled set changed.
When the set changes, delete the old key, ask the user to authenticate through your strong path, create a new key and register it. Never let a failed biometric check silently fall through to approval.
Acceptance checklist
- A valid biometric match signs the challenge and the server approves the intended action on a signed physical-device build.
- Cancellation, lockout, not enrolled and not available each take an explicit non-success path.
- Passcode fallback works only where the action’s policy allows it, and is logged as such.
- Replayed, expired, cross-account and changed-amount challenges are rejected by the server.
- An enrolment change invalidates the key and triggers re-registration through the strong path.
- Reinstall, device restore and passcode removal lead to the documented recovery process.
- Authorised hooking tests of the callback are compared with a clean baseline.
- Simulator runs are recorded as development tests, not biometric evidence.