Skill: Biometric Authentication
Overview
Biometric authentication (Face ID / Touch ID) uses LocalAuthentication to gate access to sensitive features or secrets. The strongest pattern is to bind biometrics to the Keychain via SecAccessControl (.biometryCurrentSet/.userPresence) so the OS — not your app code — enforces access; a secret is only released after a successful biometric check. Always provide a passcode/PIN fallback and handle the case where biometrics are unavailable, locked out, or re-enrolled.
Use Cases
- App lock / re-auth for sensitive screens (payments, settings).
- Releasing a Keychain-stored token or key only after biometric verification.
- Step-up authentication for high-risk actions.
Best Practices
- Bind to Keychain with
SecAccessControlso the secret is gated by the OS, not a UI flag. - Use
.biometryCurrentSetto invalidate when biometrics are re-enrolled (security event). - Always offer a fallback (device passcode / app PIN); never lock the user out entirely.
- Check
canEvaluatePolicyfirst and handleLAErrorcases (lockout, not enrolled, cancel). - Add a clear, localized reason string; never store the biometric data yourself (you can't).
Anti-Patterns
- ❌ Using a boolean "didAuthenticate" flag with the secret stored unprotected (bypassable).
- ❌ No fallback when biometrics fail/unavailable.
- ❌ Ignoring re-enrollment (a new fingerprint should re-gate sensitive data).
- ❌ Treating biometric success as identity proof to the server without server-side auth.
- ❌ Blocking the main thread during evaluation.
Checklist
- [ ] Sensitive secrets gated via Keychain
SecAccessControl, not just a UI flag. - [ ]
.biometryCurrentSet(or justified alternative) used. - [ ] Passcode/PIN fallback present.
- [ ]
canEvaluatePolicychecked;LAErrorcases handled. - [ ] Clear localized reason; evaluation off the main thread.
Swift Examples
swift
import LocalAuthentication
func authenticate(reason: String) async throws {
let context = LAContext()
context.localizedFallbackTitle = "Use Passcode"
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) else {
throw BiometricError.unavailable(error)
}
let success = try await context.evaluatePolicy(
.deviceOwnerAuthentication, // biometrics with passcode fallback
localizedReason: reason)
guard success else { throw BiometricError.failed }
}swift
// Stronger: store a secret that the OS only releases after a biometric check.
func storeBiometricProtected(_ data: Data, account: String, service: String) throws {
let access = SecAccessControlCreateWithFlags(
nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, nil)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecValueData as String: data,
kSecAttrAccessControl as String: access
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.status(status) }
}Common Interview Questions
- How do you bind a secret to biometric authentication securely?
- Why is a boolean "authenticated" flag insufficient?
- What does
.biometryCurrentSetprotect against? - How do you handle biometric lockout / not-enrolled states?
- Can the app access raw biometric data? (No.)
AI Implementation Notes
- Prefer Keychain
SecAccessControlgating over app-side flags. - Always implement a passcode/PIN fallback and handle
LAErrorcases. - Use
.biometryCurrentSetto invalidate on re-enrollment; localize the reason. - Related:
keychain.md,../../../checklists/security_review.md.