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
lib/ash_authentication/strategies/webauthn/verifier.ex
# SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Strategy.WebAuthn.Verifier do
@moduledoc """
DSL verifier for the WebAuthn strategy.
Validates configuration at compile time.
"""
alias Ash.Resource.Info, as: ResourceInfo
alias AshAuthentication.Strategy.WebAuthn
alias Spark.Error.DslError
@doc false
@spec verify(WebAuthn.t(), map) :: :ok | {:error, Exception.t()}
def verify(strategy, dsl_state) do
with :ok <- validate_wax_dependency(),
:ok <- validate_rp_id(strategy),
:ok <- validate_credential_resource(strategy),
:ok <- validate_credentials_relationship(strategy, dsl_state),
:ok <- validate_credential_resource_shape(strategy),
:ok <- validate_tokens_enabled(dsl_state),
:ok <- validate_verify_action(strategy, dsl_state) do
validate_hashed_password_optional(strategy, dsl_state)
end
end
defp validate_wax_dependency do
if Code.ensure_loaded?(Wax) do
:ok
else
{:error,
DslError.exception(
path: [:authentication, :strategies, :webauthn],
message: """
The WebAuthn strategy requires the optional `:wax_` dependency.
Add it to your dependencies:
{:wax_, "~> 0.7"}
If `ash_authentication` was already compiled without `:wax_`, recompile it:
mix deps.compile ash_authentication --force
"""
)}
end
end
defp validate_rp_id(%{rp_id: rp_id}) when is_binary(rp_id) do
cond do
String.starts_with?(rp_id, "http://") or String.starts_with?(rp_id, "https://") ->
{:error,
DslError.exception(
path: [:authentication, :strategies, :webauthn],
message:
"rp_id must be a domain name without protocol prefix (e.g. \"example.com\", not \"https://example.com\")"
)}
String.contains?(rp_id, "/") ->
{:error,
DslError.exception(
path: [:authentication, :strategies, :webauthn],
message:
"rp_id must be a domain name without paths (e.g. \"example.com\", not \"example.com/auth\")"
)}
true ->
:ok
end
end
defp validate_rp_id(_), do: :ok
defp validate_credential_resource(%{credential_resource: nil}) do
{:error,
DslError.exception(
path: [:authentication, :strategies, :webauthn],
message: "credential_resource is required"
)}
end
defp validate_credential_resource(_), do: :ok
defp validate_credentials_relationship(strategy, dsl_state) do
case ResourceInfo.relationship(dsl_state, strategy.credentials_relationship_name) do
nil ->
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message: """
The user resource is missing the `#{inspect(strategy.credentials_relationship_name)}` relationship.
Add a `has_many` relationship to the credential resource:
relationships do
has_many :#{strategy.credentials_relationship_name}, #{inspect(strategy.credential_resource)}
end
"""
)}
%{type: :has_many, destination: destination}
when destination == strategy.credential_resource ->
:ok
%{type: type} ->
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message:
"The `#{inspect(strategy.credentials_relationship_name)}` relationship must be a `has_many` to `#{inspect(strategy.credential_resource)}` (found `#{type}`)."
)}
end
end
# The credential resource may be in `no_depend_modules`, so it may not be
# compiled when the user resource is verified. Skip shape checks if we
# can't introspect it yet — they will run when the credential resource
# is itself compiled.
defp validate_credential_resource_shape(strategy) do
if Code.ensure_loaded?(strategy.credential_resource) and
function_exported?(strategy.credential_resource, :spark_dsl_config, 0) do
with :ok <- validate_belongs_to_user(strategy),
:ok <- validate_required_attribute(strategy, strategy.credential_id_field, :binary),
:ok <-
validate_required_attribute(
strategy,
strategy.public_key_field,
WebAuthn.CoseKey
) do
validate_required_attribute(strategy, strategy.sign_count_field, :integer)
end
else
:ok
end
end
defp validate_belongs_to_user(strategy) do
case ResourceInfo.relationship(strategy.credential_resource, strategy.user_relationship_name) do
%{type: :belongs_to} ->
:ok
nil ->
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message: """
The credential resource `#{inspect(strategy.credential_resource)}` is missing the \
`#{inspect(strategy.user_relationship_name)}` relationship.
Add a `belongs_to` relationship pointing to the user resource:
relationships do
belongs_to :#{strategy.user_relationship_name}, MyApp.Accounts.User, allow_nil?: false
end
"""
)}
%{type: type} ->
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message:
"The `#{inspect(strategy.user_relationship_name)}` relationship on `#{inspect(strategy.credential_resource)}` must be a `belongs_to` (found `#{type}`)."
)}
end
end
defp validate_required_attribute(strategy, name, expected_type) do
case ResourceInfo.attribute(strategy.credential_resource, name) do
nil ->
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message:
"The credential resource `#{inspect(strategy.credential_resource)}` is missing the `#{inspect(name)}` attribute."
)}
%{type: type} ->
if attribute_type_matches?(type, expected_type) do
:ok
else
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message:
"The `#{inspect(name)}` attribute on `#{inspect(strategy.credential_resource)}` must have type `#{inspect(expected_type)}` (found `#{inspect(type)}`)."
)}
end
end
end
defp attribute_type_matches?(actual, expected) do
Ash.Type.get_type(actual) == Ash.Type.get_type(expected)
end
defp validate_tokens_enabled(dsl_state) do
if AshAuthentication.Info.authentication_tokens_enabled?(dsl_state) do
:ok
else
{:error,
DslError.exception(
path: [:authentication, :strategies, :webauthn],
message: """
The WebAuthn strategy requires tokens to be enabled.
Add the following to your authentication block:
authentication do
tokens do
enabled? true
token_resource YourApp.Accounts.Token
signing_secret YourApp.Secrets
end
end
"""
)}
end
end
defp validate_verify_action(%{verify_enabled?: false}, _dsl_state), do: :ok
defp validate_verify_action(strategy, dsl_state) do
case Ash.Resource.Info.action(dsl_state, strategy.verify_action_name) do
nil ->
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message: """
The WebAuthn strategy expects a `#{inspect(strategy.verify_action_name)}` read \
action on `#{inspect(strategy.resource)}` (auto-generated by the strategy's \
transformer when `verify_enabled? true`). It looks like the action is missing.
"""
)}
_action ->
:ok
end
end
# WebAuthn registration creates a user without a password, so when the
# password strategy also exists on the same resource, `hashed_password` must
# be nil-able. Surface the conflict at compile time instead of at the first
# registration attempt.
defp validate_hashed_password_optional(strategy, dsl_state) do
with true <- strategy.registration_enabled?,
true <- AshAuthentication.Info.strategy_enabled?(dsl_state, :password),
%{allow_nil?: false} <- ResourceInfo.attribute(dsl_state, :hashed_password) do
{:error,
DslError.exception(
path: [:authentication, :strategies, strategy.name],
message: """
The `:hashed_password` attribute is `allow_nil? false`, but the WebAuthn
strategy registers users without a password. Both strategies can coexist
on the same resource only if `:hashed_password` is nil-able.
Update the attribute on `#{inspect(strategy.resource)}`:
attribute :hashed_password, :string do
sensitive? true
end
(i.e. drop the `allow_nil? false` line — the default is `true`.)
"""
)}
else
_ -> :ok
end
end
end