# ExLocalAuth
Elixir bindings to macOS **LocalAuthentication.framework** — Touch ID,
Apple Watch, and device passcode authentication via Rustler NIFs.
## Installation
Add to your `mix.exs`:
```elixir
{: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
```elixir
# 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}")
end
```
## Policies
| 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 button
- `fallback_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
```elixir
# 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](https://github.com/rusterlium/rustler) and
uses [objc2-local-authentication](https://crates.io/crates/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:
```elixir
# Gate credential access behind Touch ID
:ok = ExLocalAuth.authenticate("Access your SSH key")
{:ok, key} = ExKeychain.get("ssh-keys", "id_ed25519")
```
## License
MIT