Packages
ash_authentication
5.0.0-rc.12
5.0.0-rc.12
5.0.0-rc.11
5.0.0-rc.10
5.0.0-rc.9
5.0.0-rc.8
5.0.0-rc.7
5.0.0-rc.6
5.0.0-rc.5
5.0.0-rc.4
5.0.0-rc.3
5.0.0-rc.2
5.0.0-rc.1
5.0.0-rc.0
4.14.1
4.14.0
4.13.7
4.13.6
4.13.5
4.13.4
4.13.3
4.13.2
retired
4.13.1
retired
4.13.0
4.12.0
4.11.0
4.10.0
4.9.9
4.9.8
4.9.7
4.9.6
4.9.5
4.9.4
4.9.3
4.9.2
4.9.1
4.9.0
4.8.7
4.8.6
4.8.5
4.8.3
4.8.2
4.8.1
4.8.0
4.7.6
4.7.5
4.7.4
4.7.3
4.7.2
4.7.1
4.7.0
4.6.4
4.6.3
4.6.2
4.6.1
4.6.0
4.5.6
4.5.5
4.5.4
4.5.3
4.5.2
4.5.1
4.5.0
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.12
4.3.11
4.3.10
4.3.9
4.3.8
4.3.7
4.3.6
4.3.5
4.3.4
4.3.3
4.3.2
4.3.1
4.3.0
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
4.0.0-rc.6
4.0.0-rc.5
4.0.0-rc.3
4.0.0-rc.2
4.0.0-rc.1
4.0.0-rc.0
3.12.4
3.12.3
3.12.2
3.12.1
3.12.0
3.11.16
3.11.15
3.11.14
3.11.13
3.11.12
3.11.11
3.11.10
3.11.9
3.11.8
3.11.7
3.11.6
3.11.5
3.11.4
3.11.3
3.11.2
3.11.1
3.11.0
3.10.8
3.10.7
3.10.6
3.10.5
3.10.4
3.10.3
3.10.2
3.10.1
3.10.0
3.9.6
3.9.5
3.9.4
3.9.3
3.9.2
3.9.1
3.9.0
3.8.0
3.7.9
3.7.8
3.7.6
3.7.5
3.7.4
3.7.3
3.7.2
3.7.1
3.7.0
3.6.1
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.3.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.0
3.0.3
Authentication extension for the Ash Framework.
Current section
Files
Jump to
Current section
Files
documentation/dsls/DSL-AshAuthentication.Strategy.WebAuthn.md
<!--
This file was generated by Spark. Do not edit it by hand.
-->
# AshAuthentication.Strategy.WebAuthn
Strategy for authenticating using [WebAuthn/FIDO2](https://webauthn.io/) hardware
security keys and passkeys.
This strategy supports:
- Hardware security keys (YubiKey, etc.)
- Platform authenticators (Touch ID, Windows Hello, Face ID)
- Discoverable credentials / passkeys
- Multi-tenancy (dynamic `rp_id` per tenant)
Credentials are stored in a separate Ash resource that you define. The strategy
auto-generates actions on both the user resource and the credential resource for
registration, sign-in, credential management, and challenge generation.
## Modes
The strategy can be configured for two roles via the `registration_enabled?`,
`sign_in_enabled?`, and `verify_enabled?` flags:
- **Primary** (default; all three flags `true`) — passkeys are the primary
credential. Users register and sign in directly with their authenticator.
- **Second factor** (`registration_enabled? false`, `sign_in_enabled? false`,
`verify_enabled? true`) — passkeys are only used as a second factor on top
of another primary credential (e.g. password). The strategy doesn't
register or sign in users directly; it only verifies an assertion against
the *currently authenticated* user. On successful verification, a
`webauthn_verified_at` claim is added to the user's authentication token
so protected routes can require it.
See the
[Passkeys as 2FA](https://hexdocs.pm/ash_authentication_phoenix/webauthn-2fa.html)
guide for the second-factor flow end to end.
## Quick Start
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do
uuid_primary_key :id
attribute :email, :ci_string, allow_nil?: false
end
authentication do
tokens do
enabled? true
token_resource MyApp.Accounts.Token
signing_secret fn _, _ -> {:ok, Application.get_env(:my_app, :token_secret)} end
end
strategies do
webauthn :webauthn do
credential_resource MyApp.Accounts.Credential
rp_id "example.com"
rp_name "My App"
origin "https://example.com"
identity_field :email
end
end
end
relationships do
has_many :webauthn_credentials, MyApp.Accounts.Credential
end
identities do
identity :unique_email, [:email]
end
end
```
## Origin Configuration
The **origin** is the full URL the browser sends during WebAuthn ceremonies
(scheme + domain + port). The **rp_id** is the domain name only. These are
related but distinct:
| Setting | Example | What it is |
|-----------|----------------------------|----------------------------------|
| `rp_id` | `"example.com"` | Domain only (Relying Party ID) |
| `origin` | `"https://example.com"` | Full URL including scheme + port |
If `origin` is not set, it defaults to `"https://{rp_id}"`. This works for
production on standard port 443, but **breaks in development** because the
browser includes the port in the origin and `Wax` will reject the mismatch.
### Production
origin "https://example.com"
### Development (non-standard port)
origin "https://localhost:4001"
### Multi-tenant (dynamic per tenant)
origin {MyApp.WebAuthn, :origin_for_tenant, []}
## Credential Resource
You must define a separate Ash resource to store WebAuthn credentials. It needs:
- `credential_id` (`:binary`) - the raw credential ID from the authenticator
- `public_key` (`AshAuthentication.Strategy.WebAuthn.CoseKey`) - the COSE public key
- `sign_count` (`:integer`) - replay attack counter
- `label` (`:string`) - user-facing name for the credential
- `last_used_at` (`:utc_datetime_usec`, optional) - tracks last authentication time
- A `belongs_to` relationship to your user resource
- A policy bypass for `AshAuthentication.Checks.AshAuthenticationInteraction`
### Full Example
```elixir
defmodule MyApp.Accounts.Credential do
use Ash.Resource,
domain: MyApp.Accounts,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]
postgres do
table "webauthn_credentials"
repo(MyApp.Repo)
end
policies do
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
authorize_if always()
end
policy always() do
authorize_if always()
end
end
attributes do
uuid_primary_key :id
attribute :credential_id, :binary, allow_nil?: false, public?: true
attribute :public_key, AshAuthentication.Strategy.WebAuthn.CoseKey,
allow_nil?: false, public?: true
attribute :sign_count, :integer, default: 0, allow_nil?: false, public?: true
attribute :label, :string, default: "Security Key", public?: true
attribute :last_used_at, :utc_datetime_usec, public?: true
create_timestamp :inserted_at
update_timestamp :updated_at
end
relationships do
belongs_to :user, MyApp.Accounts.User, allow_nil?: false, public?: true
end
identities do
identity :unique_credential_id, [:credential_id]
end
actions do
defaults [:read, :destroy]
create :create do
primary? true
accept [:credential_id, :public_key, :sign_count, :label, :user_id]
end
update :update do
primary? true
accept [:sign_count, :label, :last_used_at]
end
end
end
```
## Token Configuration
Tokens **must** be enabled for WebAuthn to work. The `signing_secret` callback
must return an `{:ok, value}` tuple, not a raw string:
```elixir
authentication do
tokens do
enabled? true
token_resource MyApp.Accounts.Token
signing_secret fn _, _ -> {:ok, Application.get_env(:my_app, :token_secret)} end
end
end
```
## Adding Credentials to Existing Users
The built-in `register` action creates a **new user** with a credential. To add
a passkey to an already-authenticated user, you need a custom controller that:
1. Generates a registration challenge (via `Wax.new_registration_challenge/1`)
2. Sends it to the browser
3. Receives the attestation response
4. Calls `Wax.register/3` to verify it
5. Stores the credential directly on the credential resource
This is intentional -- the strategy's register flow is for new user sign-up,
not for adding keys to existing accounts.
## Accessing the User After Authentication
After successful WebAuthn sign-in, the JWT is available in user metadata:
```elixir
token = user.__metadata__[:token]
```
To load a user from a token (e.g., in a LiveView `mount`):
```elixir
{:ok, user} = AshAuthentication.subject_to_user(
"user?id=#{user_id}",
MyApp.Accounts.User
)
```
## Multi-Tenancy
For multi-tenant applications, `rp_id`, `rp_name`, and `origin` all accept
MFA tuples that receive the tenant as the first argument:
```elixir
webauthn :webauthn do
credential_resource MyApp.Accounts.Credential
rp_id {MyApp.WebAuthn, :rp_id_for_tenant, []}
rp_name {MyApp.WebAuthn, :rp_name_for_tenant, []}
origin {MyApp.WebAuthn, :origin_for_tenant, []}
identity_field :email
end
```
Then implement the callbacks:
```elixir
defmodule MyApp.WebAuthn do
def rp_id_for_tenant(tenant), do: "#{tenant}.example.com"
def rp_name_for_tenant(tenant), do: "MyApp - #{tenant}"
def origin_for_tenant(tenant), do: "https://#{tenant}.example.com"
end
```
## Gotchas
- **Origin must include the port** for non-standard ports (e.g., `"https://localhost:4001"`).
The default derivation from `rp_id` produces `"https://{rp_id}"` which omits the port.
- **Signing secret must return `{:ok, value}`**, not a raw string. A common mistake
is `fn _, _ -> "my_secret" end` -- it must be `fn _, _ -> {:ok, "my_secret"} end`.
- **Challenge data is stored in the session as plain maps**, not `Wax.Challenge` structs,
because cookie session stores cannot serialize arbitrary Elixir structs. The plug
reconstructs the struct before passing it to Wax.
- **`add_credential` (adding a key to an existing user) is not built-in.** The `register`
action creates a new user. See "Adding Credentials to Existing Users" above.
- **`origin_verify_fun`** is hardcoded to `{Wax, :origins_match?, []}` when
reconstructing challenges from the session. If you need custom origin verification,
you'll need to override the plug.
- **Token generation happens in `Actions.sign_in`** via `Jwt.token_for_user/3`, not in
an Ash preparation like the Password strategy. This is because Wax verification
happens outside the Ash action pipeline.
### authentication.strategies.webauthn
```elixir
webauthn name \\ :webauthn
```
Strategy for authenticating using WebAuthn/FIDO2 hardware security keys and passkeys.
### Examples
```
webauthn :webauthn do
credential_resource MyApp.Accounts.Credential
rp_id "example.com"
rp_name "My App"
identity_field :email
end
```
### Options
| Name | Type | Default | Docs |
|------|------|---------|------|
| [`credential_resource`](#authentication-strategies-webauthn-credential_resource){: #authentication-strategies-webauthn-credential_resource .spark-required} | `atom \| module` | | The Ash resource used to store WebAuthn credentials. Must have `credential_id` (binary), `public_key` (binary), and `sign_count` (integer) attributes, plus a `belongs_to` relationship to the user resource. |
| [`rp_id`](#authentication-strategies-webauthn-rp_id){: #authentication-strategies-webauthn-rp_id .spark-required} | `String.t \| (any -> any) \| mfa \| (any, any -> any) \| module` | | Relying Party ID - your domain name (e.g. "example.com"). For multi-tenant setups, pass an MFA tuple or 1-arity function that receives the tenant and returns the domain string: rp_id {MyApp.WebAuthn, :rp_id_for_tenant, []} For application-environment-driven configuration, point at a module implementing `AshAuthentication.Secret`: rp_id MyApp.Secrets |
| [`rp_name`](#authentication-strategies-webauthn-rp_name){: #authentication-strategies-webauthn-rp_name .spark-required} | `String.t \| (any -> any) \| mfa \| (any, any -> any) \| module` | | Relying Party display name shown to the user during registration. For multi-tenant setups, pass an MFA tuple or 1-arity function: rp_name {MyApp.WebAuthn, :rp_name_for_tenant, []} For application-environment-driven configuration, point at a module implementing `AshAuthentication.Secret`: rp_name MyApp.Secrets |
| [`origin`](#authentication-strategies-webauthn-origin){: #authentication-strategies-webauthn-origin } | `String.t \| (any -> any) \| mfa \| (any, any -> any) \| module` | | The expected origin for WebAuthn ceremonies. In WebAuthn, the **origin** is the scheme + domain + port that the browser reports during registration and authentication. It is distinct from `rp_id`: - `rp_id` = domain only (e.g. `"example.com"`) - `origin` = full URL (e.g. `"https://example.com"` or `"https://localhost:4001"`) If not set, defaults to `"https://{rp_id}"`. This default **omits the port**, which works for production on port 443 but will cause Wax to reject ceremonies in development where the port is non-standard. **Production:** origin "https://example.com" **Development (non-standard port):** origin "https://localhost:4001" **Dynamic (multi-tenant):** origin {MyApp.WebAuthn, :origin_for_tenant, []} **Application-environment-driven:** origin MyApp.Secrets |
| [`identity_field`](#authentication-strategies-webauthn-identity_field){: #authentication-strategies-webauthn-identity_field } | `atom` | `:email` | The name of the attribute which uniquely identifies the user (e.g. `:email`). Used for looking up the user during authentication. |
| [`authenticator_attachment`](#authentication-strategies-webauthn-authenticator_attachment){: #authentication-strategies-webauthn-authenticator_attachment } | `nil \| :platform \| :cross_platform` | | Restricts authenticator type. `nil` allows any, `:platform` limits to built-in (Touch ID, Windows Hello), `:cross_platform` limits to USB/NFC keys (YubiKey). |
| [`user_verification`](#authentication-strategies-webauthn-user_verification){: #authentication-strategies-webauthn-user_verification } | `"required" \| "preferred" \| "discouraged"` | `"preferred"` | Whether user verification (PIN/biometric) is required. Use `"required"` for highest security. |
| [`attestation`](#authentication-strategies-webauthn-attestation){: #authentication-strategies-webauthn-attestation } | `"none" \| "direct"` | `"none"` | Attestation conveyance preference. `"none"` is recommended for most use cases. `"direct"` requests the authenticator's attestation certificate. |
| [`timeout`](#authentication-strategies-webauthn-timeout){: #authentication-strategies-webauthn-timeout } | `pos_integer` | `60000` | Timeout for WebAuthn ceremonies in milliseconds. |
| [`resident_key`](#authentication-strategies-webauthn-resident_key){: #authentication-strategies-webauthn-resident_key } | `:required \| :preferred \| :discouraged` | `:required` | Whether to require discoverable credentials (passkeys). `:required` enables username-less authentication. |
| [`credential_id_field`](#authentication-strategies-webauthn-credential_id_field){: #authentication-strategies-webauthn-credential_id_field } | `atom` | `:credential_id` | The name of the credential ID attribute on the credential resource. |
| [`public_key_field`](#authentication-strategies-webauthn-public_key_field){: #authentication-strategies-webauthn-public_key_field } | `atom` | `:public_key` | The name of the public key attribute on the credential resource. |
| [`sign_count_field`](#authentication-strategies-webauthn-sign_count_field){: #authentication-strategies-webauthn-sign_count_field } | `atom` | `:sign_count` | The name of the sign count attribute on the credential resource. |
| [`label_field`](#authentication-strategies-webauthn-label_field){: #authentication-strategies-webauthn-label_field } | `atom` | `:label` | The name of the label attribute on the credential resource. |
| [`last_used_at_field`](#authentication-strategies-webauthn-last_used_at_field){: #authentication-strategies-webauthn-last_used_at_field } | `atom` | `:last_used_at` | The name of the last_used_at attribute on the credential resource. Set to `nil` to disable tracking. |
| [`user_relationship_name`](#authentication-strategies-webauthn-user_relationship_name){: #authentication-strategies-webauthn-user_relationship_name } | `atom` | `:user` | The name of the belongs_to relationship on the credential resource pointing to the user. |
| [`credentials_relationship_name`](#authentication-strategies-webauthn-credentials_relationship_name){: #authentication-strategies-webauthn-credentials_relationship_name } | `atom` | `:webauthn_credentials` | The name of the has_many relationship on the user resource pointing to credentials. |
| [`registration_enabled?`](#authentication-strategies-webauthn-registration_enabled?){: #authentication-strategies-webauthn-registration_enabled? } | `boolean` | `true` | Whether to allow new user registration via WebAuthn. |
| [`sign_in_enabled?`](#authentication-strategies-webauthn-sign_in_enabled?){: #authentication-strategies-webauthn-sign_in_enabled? } | `boolean` | `true` | Whether the strategy can sign users in directly (i.e. WebAuthn is the primary credential). Set to `false` when using WebAuthn purely as a second factor. |
| [`verify_enabled?`](#authentication-strategies-webauthn-verify_enabled?){: #authentication-strategies-webauthn-verify_enabled? } | `boolean` | `true` | Whether the strategy exposes a `:verify` phase that proves possession of a passkey for an already-authenticated user. Used for second-factor and step-up flows. |
| [`register_action_name`](#authentication-strategies-webauthn-register_action_name){: #authentication-strategies-webauthn-register_action_name } | `atom` | | The name of the register action on the user resource. Defaults to `register_with_<strategy_name>`. |
| [`sign_in_action_name`](#authentication-strategies-webauthn-sign_in_action_name){: #authentication-strategies-webauthn-sign_in_action_name } | `atom` | | The name of the sign-in action on the user resource. Defaults to `sign_in_with_<strategy_name>`. |
| [`sign_in_with_token_action_name`](#authentication-strategies-webauthn-sign_in_with_token_action_name){: #authentication-strategies-webauthn-sign_in_with_token_action_name } | `atom` | | The name of the action used to sign in with a short-lived token issued by a successful WebAuthn ceremony. Defaults to `sign_in_with_<strategy_name>_token`. |
| [`verify_action_name`](#authentication-strategies-webauthn-verify_action_name){: #authentication-strategies-webauthn-verify_action_name } | `atom` | | The name of the second-factor verify action on the user resource. Defaults to `verify_<strategy_name>`. |
| [`store_credential_action_name`](#authentication-strategies-webauthn-store_credential_action_name){: #authentication-strategies-webauthn-store_credential_action_name } | `atom` | | The name of the create action on the credential resource. Defaults to `store_<strategy_name>_credential`. |
| [`update_sign_count_action_name`](#authentication-strategies-webauthn-update_sign_count_action_name){: #authentication-strategies-webauthn-update_sign_count_action_name } | `atom` | | The name of the update action for sign_count on the credential resource. Defaults to `update_<strategy_name>_sign_count`. |
| [`list_credentials_action_name`](#authentication-strategies-webauthn-list_credentials_action_name){: #authentication-strategies-webauthn-list_credentials_action_name } | `atom` | | The name of the read action to list credentials. Defaults to `list_<strategy_name>_credentials`. |
| [`delete_credential_action_name`](#authentication-strategies-webauthn-delete_credential_action_name){: #authentication-strategies-webauthn-delete_credential_action_name } | `atom` | | The name of the destroy action for credentials. Defaults to `delete_<strategy_name>_credential`. |
| [`update_credential_label_action_name`](#authentication-strategies-webauthn-update_credential_label_action_name){: #authentication-strategies-webauthn-update_credential_label_action_name } | `atom` | | The name of the update action for credential labels. Defaults to `update_<strategy_name>_credential_label`. |
| [`add_credential_action_name`](#authentication-strategies-webauthn-add_credential_action_name){: #authentication-strategies-webauthn-add_credential_action_name } | `atom` | | The name of the action to add a credential to an existing user. Defaults to `add_<strategy_name>_credential`. |
### Introspection
Target: `AshAuthentication.Strategy.WebAuthn`
<style type="text/css">.spark-required::after { content: "*"; color: red !important; }</style>