ExLocalAuth
Elixir bindings to macOS LocalAuthentication.framework — Touch ID, Apple Watch, and device passcode authentication via Rustler NIFs.
Installation
Add to your mix.exs:
{:ex_local_auth, "~> 0.1.0"}Requires:
- macOS (Touch ID sensor or paired Apple Watch for biometric auth)
- Rust toolchain (for compiling the NIF)
- Elixir 1.15+
Quick Start
# Check if Touch ID is available
{:ok, :available} = ExLocalAuth.can_evaluate?(:biometrics)
# What kind of biometric sensor?
:touch_id = ExLocalAuth.biometry_type()
# Authenticate — shows the system Touch ID / password dialog
case ExLocalAuth.authenticate("Access your credentials", policy: :device_owner) do
:ok ->
IO.puts("Authenticated!")
{:error, :user_cancel} ->
IO.puts("User cancelled")
{:error, :biometry_lockout} ->
IO.puts("Too many failed attempts, use passcode")
{:error, reason} ->
IO.puts("Failed: #{reason}")
endPolicies
| Atom | What it accepts |
|---|---|
:biometrics | Touch ID only |
:biometrics_or_watch | Touch ID or Apple Watch |
:device_owner | Touch ID, Apple Watch, or device passcode |
:watch | Apple Watch only |
Options
authenticate/2 accepts a keyword list:
policy:— one of the policy atoms above (default::device_owner)cancel_title:— custom text for the Cancel buttonfallback_title:— custom text for the fallback button (set to""to hide it)
Error Atoms
| Atom | Meaning |
|---|---|
:user_cancel | User tapped Cancel |
:user_fallback | User tapped the fallback button |
:system_cancel | System cancelled (e.g. app went to background) |
:passcode_not_set | No passcode configured on device |
:biometry_not_available | No biometric hardware |
:biometry_not_enrolled | Biometric hardware exists but no fingerprints enrolled |
:biometry_lockout | Too many failed attempts |
:app_cancel | App programmatically cancelled |
:invalid_context | LAContext was previously invalidated |
:watch_not_available | No paired Apple Watch |
:authentication_failed | User failed to authenticate |
:biometry_disconnected | Biometric sensor disconnected |
Convenience Functions
# Boolean checks
ExLocalAuth.touch_id_available?()
ExLocalAuth.device_owner_auth_available?()
# Policy-specific wrappers
ExLocalAuth.authenticate_biometric("Unlock with Touch ID")
ExLocalAuth.authenticate_device_owner("Verify your identity")How It Works
The NIF is built with Rustler and uses objc2-local-authentication for type-safe Rust bindings to Apple's ObjC framework.
The authenticate/2 call runs on a BEAM dirty I/O scheduler thread.
It creates a fresh LAContext, configures it, then calls
evaluatePolicy:localizedReason:reply: with an ObjC block. The block
sends the result over a Rust mpsc channel, and the NIF blocks until
the user completes or cancels the system dialog.
Each authenticate call creates a new LAContext to avoid the iOS/macOS
quirk where reusing a previously-successful context skips re-verification.
Integration with ExKeychain
For maximum security, combine LocalAuthentication with Keychain access control lists:
# Gate credential access behind Touch ID
:ok = ExLocalAuth.authenticate("Access your SSH key")
{:ok, key} = ExKeychain.get("ssh-keys", "id_ed25519")License
MIT