# ExWebauthn
Elixir NIF wrapper for [webauthn-rs](https://github.com/kanidm/webauthn-rs) — passkey registration and authentication backed by Rust.
## Requirements
- Elixir >= 1.19
- OTP >= 22
Rust is **not** required for most users — precompiled NIF binaries are provided. If you need to compile from source, you'll need Rust >= 1.75 and OpenSSL dev headers.
## Precompiled platforms
Precompiled binaries are available for the following targets:
| OS | Architecture | Target |
|---|---|---|
| macOS | ARM (Apple Silicon) | `aarch64-apple-darwin` |
| macOS | x86_64 (Intel) | `x86_64-apple-darwin` |
| Linux | x86_64 (glibc) | `x86_64-unknown-linux-gnu` |
| Linux | ARM64 (glibc) | `aarch64-unknown-linux-gnu` |
| Linux | x86_64 (musl) | `x86_64-unknown-linux-musl` |
| Linux | ARM64 (musl) | `aarch64-unknown-linux-musl` |
| Windows | x86_64 (MSVC) | `x86_64-pc-windows-msvc` |
Each target is built for NIF versions 2.15, 2.16, and 2.17 (OTP 22+, 24+, 26+).
### Building from source
If your platform isn't listed above, or you want to compile locally:
```bash
EX_WEBAUTHN_BUILD=true mix compile
```
This requires Rust >= 1.75 and OpenSSL dev headers (`libssl-dev` on Debian/Ubuntu, `openssl-devel` on Fedora/RHEL).
## Installation
Add to `mix.exs`:
```elixir
def deps do
[
{:ex_webauthn, "~> 0.1.0"}
]
end
```
## Configuration
```elixir
# config/dev.exs
config :ex_webauthn,
rp_id: "localhost",
rp_origin: "http://localhost:4000"
# config/prod.exs
config :ex_webauthn,
rp_id: "example.com",
rp_origin: "https://example.com"
```
HTTPS is enforced for origins. HTTP is allowed only for loopback addresses (`localhost`, `127.0.0.1`, `::1`).
## Setup
Add `ExWebauthn` to your supervision tree:
```elixir
children = [
ExWebauthn,
MyAppWeb.Endpoint
]
```
Or with inline config:
```elixir
children = [
{ExWebauthn, rp_id: "example.com", rp_origin: "https://example.com"}
]
```
## Usage
### Registration
```elixir
# 1. Start — returns challenge options for the browser
{:ok, challenge_options, registration_state} =
ExWebauthn.start_registration(user_uuid, email, display_name)
# 2. Store registration_state server-side (session, ETS, DB)
# 3. Send challenge_options as JSON to the browser
# 4. Browser calls navigator.credentials.create()
# 5. Finish — verify the browser response
{:ok, credential} =
ExWebauthn.finish_registration(registration_state, client_response)
# 6. Store credential in your DB
```
### Authentication
```elixir
# 1. Start — pass stored credentials for the user
{:ok, challenge_options, authentication_state} =
ExWebauthn.start_authentication(stored_credentials)
# 2. Store authentication_state server-side
# 3. Send challenge_options to the browser
# 4. Browser calls navigator.credentials.get()
# 5. Finish — verify the browser response
{:ok, auth_result} =
ExWebauthn.finish_authentication(authentication_state, client_response)
```
### Discoverable credentials (usernameless)
```elixir
{:ok, challenge_options, authentication_state} =
ExWebauthn.start_authentication([])
```
### Phoenix controller example
```elixir
defmodule MyAppWeb.WebauthnController do
use MyAppWeb, :controller
def register_challenge(conn, %{"user_id" => uid, "email" => email}) do
{:ok, challenge, state} = ExWebauthn.start_registration(uid, email, email)
conn
|> put_session(:webauthn_state, state)
|> json(challenge)
end
def register_verify(conn, %{"response" => response}) do
state = get_session(conn, :webauthn_state)
case ExWebauthn.finish_registration(state, response) do
{:ok, credential} ->
# persist credential for the user
conn |> delete_session(:webauthn_state) |> json(%{status: "ok"})
{:error, _kind, message} ->
conn |> put_status(400) |> json(%{error: message})
end
end
end
```
## Error handling
All functions return `{:error, error_kind, message}` on failure:
```elixir
case ExWebauthn.start_registration(uid, email, name) do
{:ok, challenge, state} -> # success
{:error, :invalid_uuid, msg} -> # bad user ID
{:error, :invalid_origin, msg} -> # bad rp_origin
{:error, :not_initialized, msg} -> # forgot to add to supervision tree
{:error, :registration_failed, msg} -> # webauthn-rs rejected it
end
```
Error kinds: `:invalid_origin`, `:invalid_config`, `:invalid_uuid`, `:invalid_json`, `:not_initialized`, `:registration_failed`, `:authentication_failed`, `:serialization_failed`.
## Runtime reload
```elixir
ExWebauthn.reload(rp_id: "new.example.com", rp_origin: "https://new.example.com")
```
## Security
- Crypto and signature verification handled entirely by [webauthn-rs](https://github.com/kanidm/webauthn-rs) (security-audited by SUSE).
- No `unsafe` Rust code — Rustler handles NIF boilerplate.
- `parking_lot::RwLock` used to avoid lock poisoning.
- HTTPS enforced for non-loopback origins.
- Registration/authentication state is opaque and must be stored server-side only.
## License
MIT