ashauthentication: possible bugs?

frankdugan3
2023-03-23

frankdugan3:

I have a couple errors coming up with AshAuthentication, not sure if they’re bugs or me doing something wrong.

  1. If I disable registration:
    strategies do
      password :password do
        identity_field :email
        registration_enabled? false
    I get this error:
    [error] #PID<0.22797.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.22796.0>, stream id 1) terminated
    Server: localhost:4000 (http)
    Request: GET /sign-in
    ** (exit) an exception was raised:
        ** (KeyError) key :type not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
            (ash_phoenix 1.2.10) lib/ash_phoenix/form/form.ex:396: AshPhoenix.Form.for_action/3
            (ash_authentication_phoenix 1.6.4) lib/ash_authentication_phoenix/components/password/register_form.ex:59: AshAuthentication.Phoenix.Components.Password.RegisterForm.update/2
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/utils.ex:484: Phoenix.LiveView.Utils.maybe_call_update!/3
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:661: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
            (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
            (stdlib 4.2) maps.erl:411: :maps.fold_1/3
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:635: Phoenix.LiveView.Diff.render_pending_components/6
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:146: Phoenix.LiveView.Diff.render/3
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/static.ex:252: Phoenix.LiveView.Static.to_rendered_content_tag/4
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/static.ex:135: Phoenix.LiveView.Static.render/3
            (phoenix_live_view 0.18.18) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
            (phoenix 1.7.2) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
            (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint.plug_builder_call/2
            (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint."call (overridable 3)"/2
            (hsm 0.8.0) lib/plug/debugger.ex:136: HsmWeb.Endpoint."call (overridable 4)"/2
            (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint.call/2
            (phoenix 1.7.2) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
            (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
            (cowboy 2.9.0) /home/frank/git/hsm-tmp-release-branch/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
            (cowboy 2.9.0) /home/frank/git/hsm-tmp-release-branch/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3
  2. When I try to open a password reset link, I get the following error:

frankdugan3:

[error] #PID<0.24374.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.24373.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY3QiOiJwYXNzd29yZF9yZXNldF93aXRoX3Bhc3N3b3JkIiwiYXVkIjoifj4gMy4xMCIsImV4cCI6MjYxMjcxNTU4MywiaWF0IjoxNjc5NTk1NTgzLCJpc3MiOiJBc2hBdXRoZW50aWNhdGlvbiB2My4xMC4yIiwianRpIjoiMnQ3aHVpNXBiZ2djYzducTlzMDA5bmk0IiwibmJmIjoxNjc5NTk1NTgzLCJzdWIiOiJ1c2VyP2lkPWEzOGUzMzJhLTQ5ODctNTljNS1iZTliLTk4MmMyYTRjNDBhOCJ9.Qifx4rtzcKVxWAbT6eflC8yLM9egUoyL93liJhvDnSM
** (exit) an exception was raised:
    ** (ArgumentError) :socket is a reserved assign by LiveView and it cannot be set directly
        (phoenix_live_view 0.18.18) lib/phoenix_component.ex:1245: Phoenix.Component.validate_assign_key!/1
        (phoenix_live_view 0.18.18) lib/phoenix_component.ex:1196: Phoenix.Component.assign/3
        (stdlib 4.2) maps.erl:411: :maps.fold_1/3
        (ash_authentication_phoenix 1.6.4) lib/ash_authentication_phoenix/components/reset/form.ex:76: AshAuthentication.Phoenix.Components.Reset.Form.update/2
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/utils.ex:484: Phoenix.LiveView.Utils.maybe_call_update!/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:661: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
        (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
        (stdlib 4.2) maps.erl:411: :maps.fold_1/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:635: Phoenix.LiveView.Diff.render_pending_components/6
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:146: Phoenix.LiveView.Diff.render/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/static.ex:252: Phoenix.LiveView.Static.to_rendered_content_tag/4
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/static.ex:135: Phoenix.LiveView.Static.render/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
        (phoenix 1.7.2) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
        (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint.plug_builder_call/2
        (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint."call (overridable 3)"/2
        (hsm 0.8.0) lib/plug/debugger.ex:136: HsmWeb.Endpoint."call (overridable 4)"/2
        (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint.call/2
        (phoenix 1.7.2) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
        (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2

frankdugan3:

Auth config:

authentication do
  api Hsm.Ash.Authentication

  strategies do
    password :password do
      identity_field :email
      registration_enabled? false

      resettable do
        sender Hsm.Ash.Authentication.Senders.SendPasswordResetEmail
      end
    end
  end

  tokens do
    enabled? true
    token_resource Hsm.Ash.Authentication.UserToken

    signing_secret Hsm.Authentication.Secrets
  end
end

frankdugan3:

Lib versions:

ash                         2.6.27   2.6.27   Up-to-date
ash_authentication          3.10.2   3.10.2   Up-to-date
ash_authentication_phoenix  1.6.4    1.6.4    Up-to-date
ash_graphql                 0.23.1   0.23.1   Up-to-date
ash_phoenix                 1.2.10   1.2.10   Up-to-date
ash_postgres                1.3.17   1.3.17   Up-to-date
phoenix                     1.7.2    1.7.2    Up-to-date
phoenix_ecto                4.4.0    4.4.0    Up-to-date
phoenix_html                3.3.1    3.3.1    Up-to-date
phoenix_live_dashboard      0.7.2    0.7.2    Up-to-date
phoenix_live_reload         1.4.1    1.4.1    Up-to-date
phoenix_live_view           0.18.18  0.18.18  Up-to-date

frankdugan3:

I think the second one might be due to recent changes in phoenix_live_view that has more stringent checks on reserved assigns, like socket.

ZachDaniel:

🤔 interesting

ZachDaniel:

ah, yeah

ZachDaniel:

it looks like we’re using @socket all over

ZachDaniel:

damn

ZachDaniel:

So:

  1. make the form handle disabled registration
  2. eliminate all usages of passing the @socket around

ZachDaniel:

well

ZachDaniel:

Does that mean that @socket is available everywhere and doesn’t need to be provided?

frankdugan3:

I haven’t looked at the code, but based on why I would pass it around in stuff I’ve done before, I would guess it’s for the Route helpers. If that’s the case, they can also be passed the endpoint instead of the socket.

ZachDaniel:

can you try that branch out?

frankdugan3:

Yeah, is there a trick to using an unmerged PR in mix?

ZachDaniel:

just use the branch

ZachDaniel:

component-fixes

frankdugan3:

OK, looks like the password reset link works now. Still getting an error on the login form if registration is disabled.

frankdugan3:

[error] #PID<0.3447.0> running Phoenix.Endpoint.SyncCodeReloadPlug (connection #PID<0.3446.0>, stream id 1) terminated
Server: localhost:4000 (http)
Request: GET /sign-in
** (exit) an exception was raised:
    ** (KeyError) key :type not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
        (ash_phoenix 1.2.11) lib/ash_phoenix/form/form.ex:396: AshPhoenix.Form.for_action/3
        (ash_authentication_phoenix 1.6.4) lib/ash_authentication_phoenix/components/password/register_form.ex:59: AshAuthentication.Phoenix.Components.Password.RegisterForm.update/2
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/utils.ex:484: Phoenix.LiveView.Utils.maybe_call_update!/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:661: anonymous fn/5 in Phoenix.LiveView.Diff.render_pending_components/6
        (elixir 1.14.3) lib/enum.ex:2468: Enum."-reduce/3-lists^foldl/2-0-"/3
        (stdlib 4.2) maps.erl:411: :maps.fold_1/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:635: Phoenix.LiveView.Diff.render_pending_components/6
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/diff.ex:146: Phoenix.LiveView.Diff.render/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/static.ex:252: Phoenix.LiveView.Static.to_rendered_content_tag/4
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/static.ex:135: Phoenix.LiveView.Static.render/3
        (phoenix_live_view 0.18.18) lib/phoenix_live_view/controller.ex:39: Phoenix.LiveView.Controller.live_render/3
        (phoenix 1.7.2) lib/phoenix/router.ex:430: Phoenix.Router.__call__/5
        (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint.plug_builder_call/2
        (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint."call (overridable 3)"/2
        (hsm 0.8.0) lib/plug/debugger.ex:136: HsmWeb.Endpoint."call (overridable 4)"/2
        (hsm 0.8.0) lib/hsm_web/endpoint.ex:1: HsmWeb.Endpoint.call/2
        (phoenix 1.7.2) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4
        (plug_cowboy 2.6.1) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.9.0) /home/frank/git/hsm-tmp-release-branch/deps/cowboy/src/cowboy_handler.erl:37: :cowboy_handler.execute/2
        (cowboy 2.9.0) /home/frank/git/hsm-tmp-release-branch/deps/cowboy/src/cowboy_stream_h.erl:306: :cowboy_stream_h.execute/3

ZachDaniel:

Oh, yeah right I didn’t fix that

ZachDaniel:

I’m a bit confused by the set up there

ZachDaniel:

Might need <@346791515923939328> to weigh in. It looks like we need to only show the register form if one of the actions supports it. But I feel like the typespecs don’t line up with reality so I’m a bit confused. I can’t find AshAuthentication.Strategy.t()

frankdugan3:

The web error might be a little more helpful.

frankdugan3:

I think it needs to handle the action being nil because it doesn’t exist when registration is disabled.

ZachDaniel:

Yeah, but it should be disabled at a higher point

ZachDaniel:

I.e not even rendering that component

frankdugan3:

Gotchya. Yeah, I have no familiarity w/ the codebase, so I’ll probably stop guessing. 😅

frankdugan3:

Interesting, this also affects the migration generator: Also getting a migration generator error, seems to happen whether or not I disable registration:

mix ash_postgres.generate_migrations --name add_user_and_token
Compiling 115 files (.ex)

10:15:35.984 [warning] You have specified a default value for a type that cannot be explicitly
converted to an Ecto default:

  `[]`

The default value in the migration will be set to `nil` and you can edit
your migration accordingly.

To prevent this warning, implement the `EctoMigrationDefault` protocol
for the appropriate Elixir type in your Ash project, or configure its
default value in `migration_defaults` in the postgres section. Use `\"nil\"`
for no default.

** (KeyError) key :source not found in: nil. If you are using the dot syntax, such as map.field, make sure the left-hand side of the dot is a map
    (ash_postgres 1.3.18) lib/migration_generator/migration_generator.ex:2057: anonymous fn/2 in AshPostgres.MigrationGenerator.check_constraints/1
    (elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
    (ash_postgres 1.3.18) lib/migration_generator/migration_generator.ex:2052: anonymous fn/2 in AshPostgres.MigrationGenerator.check_constraints/1
    (elixir 1.14.3) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
    (ash_postgres 1.3.18) lib/migration_generator/migration_generator.ex:2004: AshPostgres.MigrationGenerator.do_snapshot/3
    (ash_postgres 1.3.18) lib/migration_generator/migration_generator.ex:1994: AshPostgres.MigrationGenerator.get_snapshots/2
    (elixir 1.14.3) lib/enum.ex:4249: Enum.flat_map_list/2
    (elixir 1.14.3) lib/enum.ex:4250: Enum.flat_map_list/2

ZachDaniel:

🤔

ZachDaniel:

I wonder which attribute we’re adding with [] as the value

frankdugan3:

Crashes before writing any files, so hard to say from my end.

ZachDaniel:

Hmm…that warning wouldn’t cause a crash

ZachDaniel:

Oh

ZachDaniel:

So dumb sorry lol

ZachDaniel:

I just looked at the warning

ZachDaniel:

…do you have any check constraints?

frankdugan3:

Bog standard on the token:

defmodule Hsm.Ash.Authentication.UserToken do
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer],
    extensions: [AshAuthentication.TokenResource]

  postgres do
    table "user_tokens"
    repo Hsm.Repo
  end

  token do
    api Hsm.Ash.Authentication
  end

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

User isn’t too fancy:

defmodule Hsm.Ash.Authentication.User do
  @moduledoc false
  use Ash.Resource,
    data_layer: AshPostgres.DataLayer,
    authorizers: [Ash.Policy.Authorizer],
    notifiers: [
      Ash.Notifier.PubSub
    ],
    extensions: [
      AshAuthentication
    ]

  import Hsm.Ash.PolicyAuthorizer.Checks
  import Hsm.Ash.Validations
  alias Hsm.Ash.Authentication.UserRole

  resource do
    description ""
  end

  postgres do
    table "users"
    repo Hsm.Repo
  end

  authentication do
    api Hsm.Ash.Authentication

    strategies do
      password :password do
        identity_field :email
        # registration_enabled? false

        resettable do
          sender Hsm.Ash.Authentication.Senders.SendPasswordResetEmail
        end
      end
    end

    tokens do
      enabled? true
      token_resource Hsm.Ash.Authentication.UserToken

      signing_secret Hsm.Authentication.Secrets
    end
  end

  pub_sub do
    # ...
  end

  attributes do
    attribute :email, :ci_string,
      allow_nil?: false,
      sensitive?: true,
      constraints: [
        max_length: 160
      ]

    attribute :roles, {:array, UserRole}, allow_nil?: false

    attribute :hashed_password, :string, sensitive?: true, allow_nil?: false
    attribute :active, :boolean, allow_nil?: false, default: true

    uuid_primary_key :id
  end

  identities do
    identity :email, [:email]
  end

  relationships do
    has_one :employee, Hsm.Ash.Employees.Employee,
      destination_attribute: :user_id,
      api: Hsm.Ash.Employees
  end

  aggregates do
    first :employee_code, :employee, :code
    first :employee_name_formal, :employee, :name_formal
  end

  validations do
    validate valid_email(:email)
  end

  actions do
    # ...
  end

  code_interface do
    # ...
  end

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

    # ...
  end
end

frankdugan3:

I’ve already manually created the migrations, so it’s not a huge rush to figure that one out.

ZachDaniel:

Honestly I think that might be a different resource entirely

frankdugan3:

Oh, that’s totally possible. Didn’t even think of that, since I haven’t been using the generator yet. I just wanted to figure out a diff for the auth stuff.

ZachDaniel:

We loop over check constraints and check for an existing matching attribute

frankdugan3:

So never mind on that, will sleuth that on my own later. Only remaining issue w/ auth is disabling the registration in AshAuthenticationPhoenix. It’s a pretty smooth experience so far, loving it! 😄

jart:

Hey folks. So the reason the socket gets passed around is for the otp_app that we pull from the endpoint.

ZachDaniel:

But it seems like @socket is just always available anyway

jart:

Okay that surprises me but ¯_(ツ)_/¯

ZachDaniel:

Same here

ZachDaniel:

But if you try out the dev server on my branch it all works

jart:

It makes sense that it would blow up with registration disabled now that I think about it because AAP isn’t aware that that’s a thing and assumes that registration and sign in are always enabled.

jart:

Should be fixable in Components.Password

ZachDaniel:

Yeah, I was looking there, but have some questions.

ZachDaniel:

    reset_enabled? =
      Enum.any?(strategy.resettable) && override_for(assigns.overrides, :reset_toggle_text)

ZachDaniel:

I’m a bit confused about strategy.resettable

ZachDaniel:

and the Enum.any?

ZachDaniel:

Also for the life of me I can’t find this type:

          required(:strategy) => AshAuthentication.Strategy.t(),

ZachDaniel:

Is that autogenerated by it being a protocol?

ZachDaniel:

Trying to figure out how to do basically the same thing for if you can register

jart:

  1. Yeah it’s because resettable is an entity on the password strategy.
  2. Yes, it’s generated by defprotocol.

ZachDaniel:

ohhh I see. I have a hack for that:

  def transform(read) do
    if read.pagination do
      if is_list(read.pagination) do
        {:ok, %{read | pagination: List.last(read.pagination) || false}}
      else
        {:ok, %{read | pagination: read.pagination}}
      end
    else
      {:ok, %{read | pagination: false}}
    end
  end

ZachDaniel:

If you have a “singleton” entity

ZachDaniel:

but I think we need to make that config in the DSL really

ZachDaniel:

okay so I should be able to fix that

ZachDaniel:

Okay <@433654314175692800> that branch should have that fix now

frankdugan3:

Success!

frankdugan3:

I also mixed in the stored tokens like you have on AshHQ, works perfect!

frankdugan3:

I’ll mark this as solved and will keep an eye out for the PR getting merged/released.

ZachDaniel:

🥳