AshAuthentication.Errors.AuthenticationFailed does not implement the AshPhoenix.FormData.Error protocol

Hello community!

I encounter a warning saying that AshAuthentication.Errors.AuthenticationFailed does not implement the AshPhoenix.FormData.Error protocol while running tests (with an voluntary wrong password) for a Phoenix Liveview.

...12:06:14.730 [warning] Unhandled error in form submission for Prepair.AshDomains.Accounts.User.change_password

This error was unhandled because AshAuthentication.Errors.AuthenticationFailed does not implement the `AshPhoenix.FormData.Error` protocol.

** (AshAuthentication.Errors.AuthenticationFailed) Authentication failed

I’ve read this issue: This error was unhandled because it did not implement the `AshPhoenix.FormData.Error` protocol. · Issue #516 · ash-project/ash · GitHub and tried the proposed solution, but it didn’t worked for me.

Here is my change_password action:

update :change_password do
      # Use this action to allow users to change their password by providing
      # their current password and a new password.

      require_atomic? false
      accept []
      argument :current_password, :string, sensitive?: true, allow_nil?: false

      argument :password, :string,
        sensitive?: true,
        allow_nil?: false

      argument :password_confirmation, :string,
        sensitive?: true,
        allow_nil?: false

      validate confirm(:password, :password_confirmation)

      validate {AshAuthentication.Strategy.Password.PasswordValidation,
                strategy_name: :password, password_argument: :current_password}

      change {AshAuthentication.Strategy.Password.HashPasswordChange,
              strategy_name: :password}
    end

I tried to add change set_context(%{strategy_name: :password}) before the other change call, but it didn’t solved the warning.

Here are the current uses and imports on my resource:

defmodule Prepair.AshDomains.Accounts.User do
  use Ash.Resource,
    domain: Prepair.AshDomains.Accounts,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshAuthentication]

  alias Prepair.AshDomains.Profiles.Profile

  import Prepair.AshDomains.ValidationMacros

Here are the current uses and imports in my domain (which exposes the code interfaces then used in the liveview).

I wonder if I should add AshPhoenix there.

defmodule Prepair.AshDomains.Accounts do
  use Ash.Domain

And here is my handle_event function for the save action in the Liveview:

def handle_event("save", %{"user" => params}, socket) do
    current_user = socket.assigns.current_user

    form =
      AshPhoenix.Form.for_update(current_user, :change_password,
        domain: Accounts,
        as: "user"
      )
      |> AshPhoenix.Form.validate(params)

    case AshPhoenix.Form.submit(form, params: params) do
      {:ok, _user} ->
        {:noreply,
         socket
         |> put_flash(:info, "Password updated successfully")
         |> redirect(to: ~p"/")}

      {:error, form} ->
        {:noreply,
         socket |> assign(:form, to_form(form)) |> assign(check_errors: true)}
    end
  end
  1. You need the pass the current_user as actor.
  2. You don’t need the call to validate since submit will validate your form anyway.
  3. I assume you assign you form to the socket in some other callback. In that case you can just do: AshPhoenix.Form.submit(socket.assigns.my_form_that_i_assigned, params: params, actor: current_user)
1 Like

Thanks for you reply @FlyingNoodle !

I’ve done the refactors you adviced, the test still passes (and my code is improved), but I still have the warning that AshAuthentication errors are not handled by AshPheonix.FormData.Error.

Here is my full current liveview:

defmodule PrepairWeb.UserUpdatePasswordLive do
  use PrepairWeb, :live_view
  use Gettext, backend: PrepairWeb.Gettext

  alias Prepair.AshDomains.Accounts

  def render(assigns) do
    ~H"""
    <div class="mx-auto max-w-sm">
      <.header class="text-center">
        <%= gettext("Update your password") %>

        <:actions>
          <.link patch={~p"/"}>
            <.button><%= gettext("Go Back Home") %></.button>
          </.link>
        </:actions>
      </.header>

      <.simple_form
        for={@update_password_form}
        id="update_password_form"
        phx-submit="save"
        phx-change="validate"
      >
        <.error :if={@check_errors}>
          <%= gettext("Oops, something went wrong! Please check the errors below.") %>
        </.error>

        <.input
          field={@update_password_form[:email]}
          type="hidden"
          id="hidden_user_email"
          value={@current_email}
        />

        <.input
          field={@update_password_form[:current_password]}
          type="password"
          label={gettext("Current password")}
          required
        />

        <.input
          field={@update_password_form[:password]}
          type="password"
          label={gettext("New password")}
          required
        />

        <.input
          field={@update_password_form[:password_confirmation]}
          type="password"
          label={gettext("New password confirmation")}
          required
        />

        <:actions>
          <.button phx-disable-with={gettext("Updating password...")} class="w-full">
            <%= gettext("Update password") %>
          </.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    current_user = socket.assigns.current_user

    form =
      AshPhoenix.Form.for_update(current_user, :change_password,
        domain: Accounts,
        as: "user"
      )

    socket =
      socket
      |> assign(check_errors: false)
      |> assign(:current_email, current_user.email)
      |> assign(:update_password_form, to_form(form))

    {:ok, socket}
  end

  def handle_event("save", %{"user" => params}, socket) do
    current_user = socket.assigns.current_user
    form = socket.assigns.update_password_form

    case AshPhoenix.Form.submit(form,
           params: params,
           actor: current_user
         ) do
      {:ok, _user} ->
        {:noreply,
         socket
         |> put_flash(:info, "Password updated successfully")
         |> redirect(to: ~p"/")}

      {:error, form} ->
        {:noreply,
         socket
         |> assign(:update_password_form, to_form(form))
         |> assign(check_errors: true)}
    end
  end

  # TODO: transform to a modal from the current profile page
  def handle_event("validate", %{"user" => params}, socket) do
    current_user = socket.assigns.current_user

    form =
      AshPhoenix.Form.for_update(current_user, :change_password,
        domain: Accounts,
        as: "user"
      )
      |> AshPhoenix.Form.validate(params)

    {:noreply, assign(socket, :update_password_form, to_form(form))}
  end
end

can you show us your policies block in the user resource?

It’s a very basic one for now:

policies do
    bypass AshAuthentication.Checks.AshAuthenticationInteraction do
      authorize_if always()
    end

    policy always() do
      authorize_if always()
    end
  end

For me the issue is not there, it’s in how to translate an AshAuthentication error to be able to output it in the form (to give the user a feedback).

To clarify: it’s normal that my test raises an AshAuthentication error. Regarding my :change_password action, I check the User’s current_password is valid before acting the password change, even if the user is already connected (it’s just an over-validation to ensure nobody else than the user did that password change in case someone just left his computer open without locking it).

So the error is normal in my test, but I would just like it to be translated into the form.

I ended by implementing the AshPhoenix.FormData.Error for AshAuthentication.Errors.AuthenticationFailed. I’m not sure it is a good practice, but for now it does the job.

defimpl AshPhoenix.FormData.Error,
  for: AshAuthentication.Errors.AuthenticationFailed do
  def to_form_error(error) do
    {error.field, "authentication failed", []}
  end
end