LocalAuthentication on macOS: Policy Evaluation and Sensitive Operations
Use LAContext on macOS to gate sensitive actions with clear reasons, short-lived decisions, reliable cancellation, and protected Keychain operations.
LocalAuthentication gives a macOS application a supported way to ask the system to authenticate the person at the computer, commonly with Touch ID or a device credential where available. LAContext coordinates the policy evaluation and system UI. It does not give the app fingerprint images, biometric templates, or a reusable password. The framework returns an outcome for a defined policy; the app remains responsible for deciding which operation that outcome authorizes and for protecting the resource itself.
This distinction prevents a common design error: treating “Touch ID succeeded once” as a general login state for the rest of a session. Authentication is evidence tied to a particular request, context, and time. If an operation protects a Keychain secret, configure that item’s access control in Keychain Services as well. If it protects an in-memory action, define a short authorization window and invalidate it when the user signs out, switches accounts, or changes the relevant application state.
Select the policy based on the action
LocalAuthentication supports policies with different fallback behavior. A biometric-only policy should be chosen only when the product truly requires a biometric. A broader device-owner authentication policy may allow a passcode or password fallback depending on the platform. Check the policy’s current availability with canEvaluatePolicy, but do not treat that check as a promise: enrollment, device state, profiles, and user choices can change before evaluation begins.
Use the preflight check to shape the user experience, then handle errors from evaluatePolicy as the authoritative result. The two operations are separate because the system can change between them. Do not silently downgrade to a weaker policy after a biometric failure. If a fallback is acceptable, state it explicitly in product behavior and test it. A policy decision should correspond to a concrete security requirement, not merely to the easiest API call.
The reason string is displayed to the user. Make it short, localized, and specific about the action: “Unlock the saved signing key to approve this release” is more useful than “Authenticate.” Do not include the app name because macOS already identifies the requesting app in the dialog. Avoid alarming or vague language that conditions users to approve prompts without understanding them.
import LocalAuthentication
func authenticateForExport(completion: @escaping (Result<Void, Error>) -> Void) {
let context = LAContext()
var availabilityError: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &availabilityError) else {
completion(.failure(availabilityError ?? LocalAuthError.unavailable))
return
}
context.evaluatePolicy(
.deviceOwnerAuthentication,
localizedReason: "Unlock the protected export you requested"
) { success, error in
if success {
completion(.success(()))
} else {
completion(.failure(error ?? LocalAuthError.denied))
}
}
}
enum LocalAuthError: Error {
case unavailable
case denied
}
The callback arrives on a framework-managed context, not necessarily the main thread. Marshal UI changes to the main actor and do not hold a mutable AppKit object in a callback that may outlive its view. Retain the context for the duration of the request and invalidate it when the operation is canceled or no longer needs it.
Treat context and result as short-lived
An LAContext is not a global authentication token. Its reuse behavior is affected by its own properties and by framework policy; configure reuse deliberately and prefer a fresh context for a new sensitive action when product security requires fresh interaction. Never persist a Boolean “authenticated” flag to disk. If a biometric domain change matters to your product, compare the documented domain state for the same application context, while remembering that the value is opaque and does not explain what changed.
Handle LAError cases according to their meaning. User cancellation is not a crash and should not cause an automatic retry loop. System cancellation, lockout, unavailable hardware, and invalidated context also require distinct behavior. Return to a safe state and allow the user to retry through a new explicit action. Do not switch automatically to asking for an app-specific password unless that alternate path was designed and secured independently.
Sensitive work may take longer than the authentication prompt. Decide whether authorization must remain valid only for the immediate operation or whether the user can approve a short batch. If a background task queues after authentication, bind the authorization result to the exact resource, operation, and request identifier. Cancel queued work when the user navigates away or changes the selected account. A prompt accepted for one file must not accidentally authorize a later file selected after the prompt appeared.
Bind Keychain access to the same security purpose
For a secret that must be protected by user presence, use a Keychain item with an access-control requirement and a suitable accessibility class. Security.framework can require user presence, any enrolled biometric, the current biometric set, or combinations of constraints. The current biometric-set option invalidates the item if the enrolled set changes. Select the constraint according to the threat model and recovery requirements rather than maximizing flags without analysis.
When querying such an item, pass the LAContext through the documented Keychain query attribute so the Keychain operation and its authentication dialog share the intended context. Handle errSecItemNotFound, authentication cancellation, interaction not allowed, and invalidated access control separately. An authentication success that is not bound to the Keychain query does not unlock the item; conversely, a Keychain operation should not be replaced by asking for biometrics and then storing a plaintext secret in memory indefinitely.
Do not store passwords or raw biometric data. Store the minimum cryptographic key or secret required, give it a stable service and account name, and define how the app handles device migration or recovery. In a sandboxed app, use an appropriate access group and keep group entitlements narrowly scoped. Tests should use disposable keys and accounts, never production credentials.
If a user can leave the Mac unattended while a long operation runs, define how the operation behaves when the session locks or the user switches accounts. Do not assume an earlier local authentication means the person remains present. For especially sensitive actions, split the work into small units and require fresh approval when the user returns to a meaningful decision point. Conversely, avoid prompting for every harmless screen refresh: excessive prompts are not a substitute for protecting the underlying key or server operation.
Keep authorization context inside the component that performs the protected operation. A UI controller should not publish a global “biometric unlocked” notification that unrelated code can consume. Pass a narrow request object or perform the protected Keychain query in the service responsible for the key. This makes it possible to audit which operations depend on LocalAuthentication and prevents an accidental privilege handoff through shared application state.
Keep authentication UI and application state synchronized
Initiate evaluation from a clear user gesture while the app can present system UI. Do not launch biometric prompts from a background login agent, while another sheet blocks the window, or as a surprise on app startup. If the app loses its window or the user switches spaces, preserve a pending state and handle cancellation rather than assuming the callback will succeed.
Do not run canEvaluatePolicy inside the callback for an active evaluatePolicy request; Apple’s documentation warns that this can deadlock. Use the completion outcome itself. Likewise, avoid calling canEvaluatePolicy repeatedly in a tight loop to wait for Touch ID availability. A status check is a point-in-time observation, not a subscription to device state.
The UI should say what will happen after approval and should offer a non-biometric path only if the product supports one. The system sheet is not a substitute for the application’s own authorization model. A user who authenticated locally is not automatically authorized for a remote account, an administrative role, or a server-side transaction. For remote actions, the server must independently verify an appropriate credential or signed challenge.
Configuration profiles and managed authentication policies may influence which local policies are available or how a prompt behaves. Enterprise support should gather the macOS build, device model, management state, selected LAPolicy, and redacted LAError code before changing profile payloads. Do not ask an administrator to deploy a broad profile change just to mask an app-level bug. Test the product against the actual managed baseline and keep the fallback behavior consistent with the organization’s authentication requirements.
When the app offers a credential fallback, design the fallback as a separate authenticated path rather than treating any text field entry as equivalent to Touch ID. Apply rate limits, secure storage, and account lockout policy appropriate to that credential. Never send a Mac account password to your service simply because LocalAuthentication could not run; the API deliberately does not expose that password to the app.
Diagnose policy outcomes rather than guessing
When evaluation fails, capture the error code, selected policy, app build, and whether the user canceled. Do not record biometric state, secrets, or private resource identifiers in analytics. During support, first determine whether the selected policy can be evaluated, whether Touch ID is configured and unlocked, and whether management policy affects authentication. Compare a clean test account with a production user before changing Keychain items or resetting privacy decisions.
If the prompt does not appear, check the code path and execution context. A context already invalidated by a previous attempt cannot be reused; an app may also have no suitable presentation context. If the prompt appears but the protected operation still fails, inspect the Keychain query’s access-control policy and error status rather than assuming LocalAuthentication caused the problem. Keep the errors separate in logs and user support instructions.
Test no biometric enrollment, successful match, failed match, user cancel, system cancel, lockout, screen lock, fast user switching, context invalidation, and a biometric-set change for the current-set policy. Verify every success path authorizes only the operation the user requested and every failure leaves the protected resource unavailable. A good LocalAuthentication integration makes security intent clear and treats all non-success outcomes as ordinary, recoverable product states.
Related:
- Secure Enclave Keys on macOS: Keychain Access Control Without Exportable Private Material
- Managing App Privacy Permissions with tccutil and the TCC Database
Sources: