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/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) do
validate_tokens_enabled(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
end