`sign_in_with_password` from AshAuthentication seems to be lacking requred `AshGraphql.Error` implimentation for bad password

I am using AshAuthentication and exposing some of its derived functions to my GraphQL API in a similar fashion to the discussions here.

  graphql do
    type :user

    queries do
      read_one :sign_in_with_password, :sign_in_with_password do
        as_mutation? true
        type_name :user_with_metadata
      end
    end
  
  end

When I submit a valid pair of credentials, it works, but when I send a bad password, it fails.

mutation {
  signInWithPassword(email: "mike@mikezornek.com", password: "passwordd") {
    id
    email
    token
  }
}
[warning] `f3b44d53-10d6-434a-9d14-e59ab59e42a2`: AshGraphql.Error not implemented for error:

** (AshAuthentication.Errors.AuthenticationFailed) Authentication failed
    (ash_authentication 3.11.16) lib/ash_authentication/errors/authentication_failed.ex:13: AshAuthentication.Errors.AuthenticationFailed.exception/1
    (ash_authentication 3.11.16) lib/ash_authentication/strategies/password/sign_in_preparation.ex:53: anonymous fn/3 in AshAuthentication.Strategy.Password.SignInPreparation.prepare/3
    (ash 2.16.1) lib/ash/actions/read.ex:2526: anonymous fn/2 in Ash.Actions.Read.run_after_action/2
    (elixir 1.15.5) lib/enum.ex:4830: Enumerable.List.reduce/3
    (elixir 1.15.5) lib/enum.ex:2564: Enum.reduce_while/3
    (ash 2.16.1) lib/ash/actions/read.ex:2524: Ash.Actions.Read.run_after_action/2
    (ash 2.16.1) lib/ash/actions/read.ex:1459: anonymous fn/4 in Ash.Actions.Read.data_field/3
    (ash 2.16.1) lib/ash/engine/engine.ex:537: anonymous fn/2 in Ash.Engine.run_iteration/1
    (ash 2.16.1) lib/ash/engine/engine.ex:558: anonymous fn/4 in Ash.Engine.async/2
    (elixir 1.15.5) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2
    (elixir 1.15.5) lib/task/supervised.ex:36: Task.Supervised.reply/4

Looking at lib/ash_authentication/strategies/password/sign_in_preparation.ex:53 I observe it returning an error tuple like:

             {:error,
              AuthenticationFailed.exception(
                strategy: strategy,
                query: query,
                caused_by: %{
                  module: __MODULE__,
                  action: query.action,
                  resource: query.resource,
                  message: "Password is not valid"
                }
              )}

My questions:

  1. How should I go about making sure the module AshAuthentication.Errors.AuthenticationFailed implements AshGraphql.Error?

  2. Why is this function name called exception? I feel like I observe of lot of inner Ash things throwing exceptions as a mode of logic handling, and it feels curious to me coming from other Elixir codebases. Like is this returning an error tuple, or is this throwing an exception? I would have thought I’d see a function like AuthenticationFailed.new.

  3. I suppose if I can’t from the outside add this error implementation, I’ll have to handcraft a resolver. Is that what is expected in this scenario?

1 Like

Continuing to work on this today, I was able to figure out how to provide an implementation of to_error in my own app for AshAuthentication.Errors.AuthenticationFailed.

# lib/study_hall/ash_graphql_errors/authentication_failed.ex
defimpl AshGraphql.Error, for: AshAuthentication.Errors.AuthenticationFailed do
  alias AshAuthentication.Errors.AuthenticationFailed

  def to_error(%AuthenticationFailed{caused_by: %{message: message}} = error) do
    %{
      message: message,
      short_message: message,
      vars: Map.new(error.vars),
      code: Ash.ErrorKind.code(error),
      fields: List.wrap(error.field)
    }
  end
end

I’m going to mark this as my own solution, but welcome feedback on my #2 question or any other observations people might have.

1 Like

Hey @zorn :wave:

Yeah, the AuthenticationFailed error doesn’t implement AshGraphQl.Error because we don’t know that folks will actually be using AshGraphQl when they are using AshAuthentication.

You’ve come up with the correct solution - implement the AshGraphQl.Error protocol yourself. I want to point out that you should never share any information from the caused_by field with other parties - it is only there for debugging purposes and sometimes contains information that could be used by an attacker. Best practice is to simply return an “Invalid username or password” message to the user when sign in fails.

2 Likes

Regarding this; exception/1 is the standard constructor callback for anything that implements the Exception behaviour, which all errors in the Ash ecosystem do (or at least they should). We use exception structs wherever we have errors to avoid having to have two different code paths - they can be returned in an :error tuple or raised without change.

2 Likes

Thanks. Yeah, I edited my error module a bit after posting while working through some tests to validate bad password and bad emails. Observed I shouldn’t be leaking and changed it to be a static message.

# lib/study_hall/ash_graphql_error_impl/authentication_failed.ex
defimpl AshGraphql.Error, for: AshAuthentication.Errors.AuthenticationFailed do
  @moduledoc """
  Provides an implementation of `AshGraphql.Error` protocol for authentication errors
  that come out of the `AshAuthentication` library.

  You can find other implementations here:
  https://github.com/ash-project/ash_graphql/blob/main/lib/error.ex
  """

  def to_error(error) do
    # The `AuthenticationFailed` actually has detailed information about the
    # error, like `Password is not valid` or `query returned no users` but we do
    # not want to leak those details over the API, so we'll return a static
    # error message.
    %{
      message: "could not sign in with the provided credentials",
      shortMessage: "could not sign in with the provided credentials",
      vars: Map.new(error.vars),
      code: Ash.ErrorKind.code(error),
      fields: List.wrap(error.field)
    }
  end
end

1 Like

vars: Map.new(error.vars), I would suggest vars: %{}, just to be on the safe side :slight_smile:

Edit, so something like this would be the answer for those hunting down a solution:

defimpl AshGraphql.Error, for: AshAuthentication.Errors.AuthenticationFailed do
  @moduledoc """
  Provides an implementation of `AshGraphql.Error` protocol for authentication errors
  that come out of the `AshAuthentication` library.

  You can find other implementations here:
  https://github.com/ash-project/ash_graphql/blob/main/lib/error.ex
  """

  def to_error(error) do
    # The `AuthenticationFailed` actually has detailed information about the
    # error, like `Password is not valid` or `query returned no users` but we do
    # not want to leak those details over the API, so we'll return a static
    # error message.
    %{
      message: "could not sign in with the provided credentials",
      shortMessage: "could not sign in with the provided credentials",
      vars: %{},
      code: Ash.ErrorKind.code(error),
      fields: List.wrap(error.field)
    }
  end
end
2 Likes