How to create a static Ash authentication form?

I need to customise the registration for Ash authentication. After reading through Customising Ash Authentication with Phoenix LiveView I came up with this controller:

defmodule AniminaWeb.AccountsController do
  use AniminaWeb, :controller

  alias Animina.Accounts
  alias Animina.Accounts.User
  alias AshPhoenix.Form

  def register(conn, _params) do
    conn =
      conn
      |> assign(:form_id, "sign-up-form")
      |> assign(:cta, "Create account")
      |> assign(:alternative_path, ~p"/sign-in")
      |> assign(:alternative, "Have an account?")
      |> assign(:action, ~p"/auth/user/password/register")
      |> assign(:is_register?, true)
      |> assign(
        :form,
        Form.for_create(User, :register_with_password, api: Accounts, as: "user")
      )

    render(conn, :register, layout: false)
  end
end

And this view template:

<div class="py-6 space-y-10 px-5">
  <div class="flex min-h-full flex-col justify-center px-6 py-12 lg:px-8">
    <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
      <.form :let={f} for={@form} action={@action} method="POST" class="space-y-6">
        <%= if @is_register? do %>
          <div>
            <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
              Username
            </label>
            <div class="mt-2">
              <input
                id="username"
                name="username"
                type="text"
                autocomplete="username"
                required
                placeholder="Pusteblume1977"
                autofocus
                class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
              />
            </div>
          </div>
        <% end %>

        <div>
          <label for="email" class="block text-sm font-medium leading-6 text-gray-900">
            E-Mail Addresse
          </label>
          <div class="mt-2">
            <input
              id="email"
              name="email"
              type="email"
              autocomplete="email"
              required
              placeholder="eddie@beispiel.de"
              class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
            />
          </div>
        </div>

        <div>
          <div class="flex items-center justify-between">
            <label for="password" class="block text-sm font-medium leading-6 text-gray-900">
              Passwort
            </label>
            <%= unless @is_register? do %>
              <div class="text-sm">
                <a href="#" class="font-semibold text-indigo-600 hover:text-indigo-500">
                  Passwort vergessen?
                </a>
              </div>
            <% end %>
          </div>
          <div class="mt-2">
            <%= password_input(f, :password,
              class:
                "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
              placeholder: "Passwort"
            ) %>
          </div>
        </div>

        <div>
          <%= submit(@cta,
            class:
              "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
          ) %>
        </div>
      </.form>
    </div>
  </div>
</div>

But it doesn’t work. When I enter the data in the form and send it I always get an Authentication Error error.

[info] GET /register
[debug] Processing with AniminaWeb.AccountsController.register/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 71ms
[info] POST /auth/user/password/register
[debug] Processing with AniminaWeb.AuthController
  Parameters: %{"_csrf_token" => "asdf", "_method" => "POST", "email" => "stefan@company.com", "user" => %{"password" => "[FILTERED]"}, "username" => "sw"}
  Pipelines: [:browser]
[info] Sent 401 in 56ms

What do I need to change?

You should be able to look at your auth controller and see the error type/reason there in the failure hook. That should have more information.

OK. This is the content of reason:

[info] POST /auth/user/password/register
[debug] Processing with AniminaWeb.AuthController
  Parameters: %{"_csrf_token" => "dA4OHgQeaFgjfBtGPgphMA4HHit4Bxlt5tQYRs2iNMP-K_TfZ7oh0WZ8", "_method" => "POST", "email" => "sw@wintermeyer-consulting.de", "user" => %{"password" => "[FILTERED]"}, "username" => "sw"}
  Pipelines: [:browser]
[(animina 0.1.0) lib/animina_web/controllers/auth_controller.ex:16: AniminaWeb.AuthController.failure/3]
reason #=> %Ash.Error.Invalid{
  errors: [
    %Ash.Error.Changes.Required{
      field: :email,
      type: :attribute,
      resource: Animina.Accounts.User,
      changeset: nil,
      query: nil,
      error_context: [],
      vars: [],
      path: [],
      stacktrace: #Stacktrace<>,
      class: :invalid
    },
    %Ash.Error.Changes.Required{
      field: :username,
      type: :attribute,
      resource: Animina.Accounts.User,
      changeset: nil,
      query: nil,
      error_context: [],
      vars: [],
      path: [],
      stacktrace: #Stacktrace<>,
      class: :invalid
    }
  ],
  stacktraces?: true,
  changeset: #Ash.Changeset<
    api: Animina.Accounts,
    action_type: :create,
    action: :register_with_password,
    attributes: %{id: "2e7b7d91-e156-433d-aa06-5c43877a7f54"},
    relationships: %{},
    arguments: %{password: "**redacted**"},
    errors: [
      %Ash.Error.Changes.Required{
        field: :email,
        type: :attribute,
        resource: Animina.Accounts.User,
        changeset: nil,
        query: nil,
        error_context: [],
        vars: [],
        path: [],
        stacktrace: #Stacktrace<>,
        class: :invalid
      },
      %Ash.Error.Changes.Required{
        field: :username,
        type: :attribute,
        resource: Animina.Accounts.User,
        changeset: nil,
        query: nil,
        error_context: [],
        vars: [],
        path: [],
        stacktrace: #Stacktrace<>,
        class: :invalid
      }
    ],
    data: #Animina.Accounts.User<
      __meta__: #Ecto.Schema.Metadata<:built, "users">,
      id: nil,
      email: nil,
      username: nil,
      aggregates: %{},
      calculations: %{},
      ...
    >,
    context: %{authorize?: false, actor: nil},
    valid?: false
  >,
  query: nil,
  error_context: [nil],
  vars: [],
  path: [],
  stacktrace: #Stacktrace<>,
  class: :invalid
}

Why is there no data in the changeset?

The data key contains the “original” data which for a create changeset is an empty record. What you want to look at is attributes and arguments. As far as I can tell, you aren’t passing the email or username, both of which are required.

I hardcoded form input fields by hand into the HTML. That was the reason it didn’t work. :man_facepalming:

The solution:

      <.form :let={f} for={@form} action={@action} method="POST" class="space-y-6">
        <%= if @is_register? do %>
          <div>
            <label for="username" class="block text-sm font-medium leading-6 text-gray-900">
              Username
            </label>
            <div class="mt-2">
              <%= text_input(f, :username,
                class:
                  "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
                placeholder: "Pusteblume1977",
                type: :text,
                required: true,
                autofocus: true
              ) %>
            </div>
          </div>
        <% end %>

        <div>
          <label for="email" class="block text-sm font-medium leading-6 text-gray-900">
            E-Mail Addresse
          </label>
          <div class="mt-2">
            <%= text_input(f, :email,
              class:
                "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
              placeholder: "eddie@beispiel.de",
              type: :email,
              required: true
            ) %>
          </div>
        </div>

        <div>
          <div class="flex items-center justify-between">
            <label for="password" class="block text-sm font-medium leading-6 text-gray-900">
              Passwort
            </label>
            <%= unless @is_register? do %>
              <div class="text-sm">
                <a href="#" class="font-semibold text-indigo-600 hover:text-indigo-500">
                  Passwort vergessen?
                </a>
              </div>
            <% end %>
          </div>
          <div class="mt-2">
            <%= password_input(f, :password,
              class:
                "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6",
              placeholder: "Passwort"
            ) %>
          </div>
        </div>

        <div>
          <%= submit(@cta,
            class:
              "flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
          ) %>
        </div>
      </.form>
1 Like