Packages
ash_authentication
5.0.0-rc.6
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.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/ash_authentication.add_strategy.webauthn.ex
# SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
# credo:disable-for-this-file Credo.Check.Design.AliasUsage
if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.AshAuthentication.AddStrategy.Webauthn do
use Igniter.Mix.Task
@example "mix ash_authentication.add_strategy.webauthn --rp-id example.com --rp-name \"My App\""
@shortdoc "Adds WebAuthn/Passkey authentication to your user resource"
@moduledoc """
#{@shortdoc}
Creates a credential resource and adds the WebAuthn strategy to the user
resource. Users can sign in with hardware security keys (YubiKey),
platform authenticators (Touch ID, Windows Hello), or passkeys.
## Example
```bash
#{@example}
```
## Options
* `--user`, `-u` - The user resource. Defaults to `YourApp.Accounts.User`.
* `--identity-field`, `-i` - The field on the user resource that
identifies the user (typically email). Defaults to `email`.
* `--name`, `-n` - The strategy name. Defaults to `webauthn`.
* `--rp-id` - The Relying Party ID (your domain, e.g. `example.com`).
Required.
* `--rp-name` - The Relying Party display name shown to the user during
registration. Required.
* `--origin` - The full origin URL (e.g. `https://example.com` or
`https://localhost:4001`). Optional — defaults to `https://{rp_id}`,
which omits the port and is wrong for non-standard dev ports.
"""
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :ash,
example: @example,
extra_args?: false,
only: nil,
positional: [],
schema: [
accounts: :string,
user: :string,
identity_field: :string,
name: :string,
rp_id: :string,
rp_name: :string,
origin: :string
],
aliases: [
a: :accounts,
u: :user,
i: :identity_field,
n: :name
],
defaults: [
identity_field: "email",
name: "webauthn"
],
required: [:rp_id, :rp_name]
}
end
def igniter(igniter) do
options = parse_options(igniter)
case Igniter.Project.Module.module_exists(igniter, options[:user]) do
{true, igniter} ->
igniter
|> add_wax_dependency()
|> add_credential_resource(options)
|> AshAuthentication.Igniter.codegen_for_strategy(options[:name])
{false, igniter} ->
Igniter.add_issue(igniter, """
User module #{inspect(options[:user])} was not found.
Perhaps you have not yet installed ash_authentication?
""")
end
end
defp parse_options(igniter) do
options =
igniter.args.options
|> Keyword.put_new_lazy(:accounts, fn ->
Igniter.Project.Module.module_name(igniter, "Accounts")
end)
options
|> Keyword.put_new_lazy(:user, fn ->
Module.concat(options[:accounts], User)
end)
|> Keyword.update(:identity_field, :email, &String.to_atom/1)
|> Keyword.update(:name, :webauthn, &String.to_atom/1)
|> Keyword.update!(:accounts, &AshAuthentication.Igniter.maybe_parse_module/1)
|> Keyword.update!(:user, &AshAuthentication.Igniter.maybe_parse_module/1)
end
defp add_wax_dependency(igniter) do
Igniter.Project.Deps.add_dep(igniter, {:wax_, "~> 0.7"}, on_exists: :skip)
end
defp add_credential_resource(igniter, options) do
credential_resource =
options[:user]
|> Module.split()
|> :lists.droplast()
|> Enum.concat([WebAuthnCredential])
|> Module.concat()
{exists?, igniter} = Igniter.Project.Module.module_exists(igniter, credential_resource)
if exists? do
Igniter.add_issue(
igniter,
"WebAuthn credential resource already exists: #{inspect(credential_resource)}."
)
else
igniter
|> generate_credential_resource(credential_resource, options)
|> add_user_attributes_and_relationship(credential_resource, options)
|> add_strategy_to_user(credential_resource, options)
end
end
defp generate_credential_resource(igniter, credential_resource, options) do
extensions = data_layer_extension()
igniter
|> Igniter.compose_task("ash.gen.resource", [
inspect(credential_resource),
"--uuid-primary-key",
"id",
"--default-actions",
"read,destroy",
"--attribute",
"credential_id:binary:required",
"--attribute",
"sign_count:integer",
"--attribute",
"label:string",
"--attribute",
"last_used_at:utc_datetime_usec",
"--relationship",
"belongs_to:user:#{inspect(options[:user])}:required",
"--extend",
extensions
])
|> Ash.Resource.Igniter.add_new_attribute(credential_resource, :public_key, """
attribute :public_key, AshAuthentication.Strategy.WebAuthn.CoseKey do
allow_nil? false
public? true
end
""")
|> Ash.Resource.Igniter.add_new_action(credential_resource, :create, """
create :create do
primary? true
accept [:credential_id, :public_key, :sign_count, :label, :user_id]
end
""")
|> Ash.Resource.Igniter.add_new_action(credential_resource, :update, """
update :update do
primary? true
accept [:sign_count, :label, :last_used_at]
end
""")
|> add_credential_resource_authorizer(credential_resource)
|> Ash.Resource.Igniter.add_new_identity(credential_resource, :unique_credential_id, """
identity :unique_credential_id, [:credential_id]
""")
end
defp add_credential_resource_authorizer(igniter, credential_resource) do
Igniter.Project.Module.find_and_update_module!(
igniter,
credential_resource,
&ensure_authorizer_bypass/1
)
end
defp ensure_authorizer_bypass(zipper) do
with {:ok, do_zipper} <- Igniter.Code.Common.move_to_do_block(zipper),
:error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
do_zipper,
:policies,
1
) do
{:ok,
Igniter.Code.Common.add_code(do_zipper, """
policies do
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
authorize_if always()
end
end
""")}
else
{:ok, _} -> {:ok, zipper}
:error -> {:ok, zipper}
end
end
defp add_user_attributes_and_relationship(igniter, credential_resource, options) do
identity_field = options[:identity_field]
igniter
|> Ash.Resource.Igniter.add_new_attribute(options[:user], identity_field, """
attribute #{inspect(identity_field)}, :ci_string do
allow_nil? false
public? true
end
""")
|> AshAuthentication.Igniter.ensure_identity(options[:user], identity_field)
|> Ash.Resource.Igniter.add_new_relationship(
options[:user],
:webauthn_credentials,
"""
has_many :webauthn_credentials, #{inspect(credential_resource)}
"""
)
end
defp add_strategy_to_user(igniter, credential_resource, options) do
AshAuthentication.Igniter.add_new_strategy(
igniter,
options[:user],
options[:name],
options[:name],
strategy_block(credential_resource, options)
)
end
defp strategy_block(credential_resource, options) do
origin_line =
case options[:origin] do
nil -> ""
origin -> " origin #{inspect(origin)}\n"
end
"""
webauthn #{inspect(options[:name])} do
credential_resource #{inspect(credential_resource)}
rp_id #{inspect(options[:rp_id])}
rp_name #{inspect(options[:rp_name])}
#{origin_line} identity_field #{inspect(options[:identity_field])}
end
"""
end
defp data_layer_extension do
cond do
Code.ensure_loaded?(AshPostgres.DataLayer) -> "postgres"
Code.ensure_loaded?(AshSqlite.DataLayer) -> "sqlite"
true -> ""
end
end
end
else
defmodule Mix.Tasks.AshAuthentication.AddStrategy.Webauthn do
@shortdoc "Adds WebAuthn/Passkey authentication to your user resource"
@moduledoc @shortdoc
use Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'ash_authentication.add_strategy.webauthn' requires igniter to be run.
Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter
""")
exit({:shutdown, 1})
end
end
end