Technical guide

iOS biometric attributes implementation guide

Implement Face ID and Touch ID on iOS with the right LAPolicy, a Secure Enclave key bound to the current biometric set and a server-signed challenge.

  • Key, not a flag

    A Secure Enclave key that only signs after a biometric match gives the server proof. A success Boolean gives it nothing.

  • Fresh, bound challenges

    The server issues a one-time challenge tied to the session and the action, and verifies the signature before it acts.

  • Plan for change

    Enrolment changes, lockout and new devices are normal. Detect them and re-establish trust through a strong path.

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.

ValueWhat it meansWhat it does not mean
biometryType (.faceID, .touchID, .opticID, .none)Which biometric interface the device offersThat the user has enrolled or authenticated
canEvaluatePolicy returns trueThe chosen policy can run nowThat anyone has authenticated
evaluatePolicy succeedsThe local policy was satisfiedThat your server should approve the action
LAError such as .userCancel, .biometryLockout, .biometryNotEnrolledWhy this attempt did not completeThat 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

  • .deviceOwnerAuthenticationWithBiometrics requires Face ID or Touch ID. After repeated failures biometry locks out, and the user must enter the passcode before biometrics work again.
  • .deviceOwnerAuthentication accepts 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

  1. Create an access control with SecAccessControlCreateWithFlags, protection class kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly and flags .privateKeyUsage and .biometryCurrentSet.
  2. Create a P-256 key in the Secure Enclave with SecKeyCreateRandomKey and kSecAttrTokenIDSecureEnclave, or with CryptoKit’s SecureEnclave.P256.Signing.PrivateKey and the same access control. The private key never leaves the Secure Enclave.
  3. 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

  1. The app asks the server to start an action, such as a payment of a given amount to a given payee.
  2. The server returns a one-time challenge bound to the session, the account and the action details, with a short expiry.
  3. 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 an LAContext through kSecUseAuthenticationContext if you want to control the prompt text.
  4. 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.

Further reading

Common questions

  • Is a successful evaluatePolicy call enough for my server to approve an action?

    No. evaluatePolicy returns a Boolean in your process, and anyone who can hook the callback can turn a failure into a success without the server noticing. Bind the check to a Secure Enclave key instead: the app signs a fresh server challenge, and the server verifies the signature, not a success flag.

  • What is the difference between .deviceOwnerAuthenticationWithBiometrics and .deviceOwnerAuthentication?

    .deviceOwnerAuthenticationWithBiometrics accepts Face ID or Touch ID only, while .deviceOwnerAuthentication also accepts the device passcode. Use the second only where knowledge of the passcode is acceptable for the action. Record the policy per action, and label passcode approvals as passcode approvals in your logs.

  • What happens to the key when a user adds a fingerprint or re-enrols a face?

    With biometryCurrentSet, the key is invalidated and signing fails, so the app must handle that as a defined state, not a crash. Delete the old key, ask the user to authenticate through your strong path, then create and register a new key. Use .biometryAny only if new enrolments may approve actions.

  • How do I detect a biometric enrolment change before signing?

    Store the 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, and a failed check must never fall through to approval.

  • Does Otukan see or store biometric data on iOS?

    No. Otukan does not access or store biometric data, and matching stays with iOS. 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.

Validate the whole authentication journey

Tell us your iOS scope, app framework, authentication flow and fallback requirements. We do not ask for binaries, biometric data, signing keys, credentials or production data.

Or request a technical demo