Packages
ash_authentication_phoenix
3.0.0-rc.8
3.0.0-rc.9
3.0.0-rc.8
3.0.0-rc.7
3.0.0-rc.6
3.0.0-rc.4
3.0.0-rc.3
3.0.0-rc.2
3.0.0-rc.1
3.0.0-rc.0
2.17.2
2.17.1
2.17.0
2.16.0
2.15.0
2.14.1
2.14.0
2.13.1
2.13.0
2.12.2
2.12.1
2.12.0
2.11.0
2.10.5
2.10.4
2.10.3
2.10.2
2.10.1
2.10.0
2.9.0
2.8.0
2.7.0
2.6.3
2.6.2
2.6.1
2.6.0
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.8
2.4.7
2.4.6
2.4.5
2.4.4
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.1
2.2.0
2.1.11
2.1.10
2.1.9
2.1.8
2.1.7
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.2
2.0.1
2.0.0
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.9.4
1.9.3
1.9.2
1.9.1
1.9.0
1.8.7
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.1
1.8.0
1.7.3
1.7.2
1.7.1
1.7.0
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.1
1.0.0
Phoenix integration for Ash Authentication
Current section
Files
Jump to
Current section
Files
lib/ash_authentication_phoenix/webauthn_helpers.ex
# SPDX-FileCopyrightText: 2026 Alembic Pty Ltd
#
# SPDX-License-Identifier: MIT
defmodule AshAuthentication.Phoenix.WebAuthnHelpers do
@moduledoc """
Helper functions for working with WebAuthn second-factor authentication.
These helpers can be used in plugs, LiveView on_mount hooks, and templates
to check whether a user has a passkey registered (`webauthn_configured?`)
and whether the current request has been verified by a passkey ceremony
(`webauthn_verified?`).
See the [Passkeys as 2FA](webauthn-2fa.md) guide for the full flow.
"""
alias AshAuthentication.Info
alias AshAuthentication.Strategy.WebAuthn
alias Phoenix.LiveView.Socket
alias Plug.Conn
@doc """
Returns `true` if the user has at least one registered WebAuthn credential.
Walks the strategy's `credentials_relationship_name` (loading it if it
isn't already loaded). Returns `false` for `nil` users so callers don't
have to nil-check first.
## Options
* `:strategy` — the WebAuthn strategy to check against. Defaults to the
first WebAuthn strategy on the user's resource.
"""
@spec webauthn_configured?(nil | Ash.Resource.Record.t(), keyword()) :: boolean()
def webauthn_configured?(user, opts \\ [])
def webauthn_configured?(nil, _opts), do: false
def webauthn_configured?(user, opts) when is_struct(user) do
with {:ok, strategy} <- get_webauthn_strategy(user.__struct__, opts),
{:ok, credentials} <- fetch_credentials(user, strategy.credentials_relationship_name) do
Enum.any?(credentials)
else
_ -> false
end
end
defp fetch_credentials(user, relationship_name) do
case Map.get(user, relationship_name) do
%Ash.NotLoaded{} -> load_credentials(user, relationship_name)
credentials when is_list(credentials) -> {:ok, credentials}
_ -> :error
end
end
defp load_credentials(user, relationship_name) do
case Ash.load(user, [relationship_name],
authorize?: false,
domain: Info.domain!(user.__struct__)
) do
{:ok, loaded} -> {:ok, Map.get(loaded, relationship_name, [])}
_ -> :error
end
end
@doc """
Returns `true` if the current request has a WebAuthn verification on it.
Accepts either:
* a `Plug.Conn` — looks at the assign named by `:current_user_assign`
(default `:current_user`).
* a `Phoenix.LiveView.Socket` — same, against `socket.assigns`.
* a user struct — checks its `__metadata__[:webauthn_verified_at]`
directly. Useful in `AuthController.success/4` clauses where you
have the user but no conn / socket.
## Options
* `:max_age` — maximum age of the verification in seconds. `nil`
(default) means no expiry; any timestamp counts.
* `:current_user_assign` — assign holding the user (conn / socket
forms only). Defaults to `:current_user`.
"""
@spec webauthn_verified?(Conn.t() | Socket.t() | struct() | nil, keyword()) :: boolean()
def webauthn_verified?(conn_or_socket_or_user, opts \\ [])
def webauthn_verified?(nil, _opts), do: false
def webauthn_verified?(%Conn{} = conn, opts), do: verified?(conn.assigns, opts)
def webauthn_verified?(%Socket{} = socket, opts), do: verified?(socket.assigns, opts)
def webauthn_verified?(%_{} = user, opts) do
within_max_age?(verified_at(user), Keyword.get(opts, :max_age))
end
defp verified?(assigns, opts) do
user_assign = Keyword.get(opts, :current_user_assign, :current_user)
max_age = Keyword.get(opts, :max_age)
case Map.get(assigns, user_assign) do
nil -> false
user -> within_max_age?(verified_at(user), max_age)
end
end
defp verified_at(%{__metadata__: metadata}) do
Map.get(metadata || %{}, :webauthn_verified_at)
end
defp verified_at(_), do: nil
defp within_max_age?(nil, _max_age), do: false
defp within_max_age?(%DateTime{}, nil), do: true
defp within_max_age?(%DateTime{} = verified_at, max_age)
when is_integer(max_age) and max_age >= 0 do
DateTime.diff(DateTime.utc_now(), verified_at, :second) <= max_age
end
defp within_max_age?(verified_at, max_age) when is_binary(verified_at) do
case DateTime.from_iso8601(verified_at) do
{:ok, dt, _} -> within_max_age?(dt, max_age)
_ -> false
end
end
defp within_max_age?(_, _), do: false
@doc """
Returns the WebAuthn strategy on a resource, if any.
## Options
* `:strategy` — return the named strategy. If unset, returns the first
WebAuthn strategy.
"""
@spec get_webauthn_strategy(module(), keyword()) ::
{:ok, WebAuthn.t()} | {:error, :no_webauthn_strategy}
def get_webauthn_strategy(resource, opts \\ []) when is_atom(resource) do
strategy_name = Keyword.get(opts, :strategy)
strategies =
resource
|> Info.authentication_strategies()
|> Enum.filter(&match?(%WebAuthn{}, &1))
case {strategy_name, strategies} do
{nil, [strategy | _]} ->
{:ok, strategy}
{name, strategies} when is_atom(name) ->
case Enum.find(strategies, &(&1.name == name)) do
nil -> {:error, :no_webauthn_strategy}
strategy -> {:ok, strategy}
end
{_, []} ->
{:error, :no_webauthn_strategy}
end
end
@doc """
Returns `true` if the resource has a WebAuthn strategy configured.
"""
@spec webauthn_available?(module()) :: boolean()
def webauthn_available?(resource) when is_atom(resource) do
case get_webauthn_strategy(resource) do
{:ok, _} -> true
_ -> false
end
end
end