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.
How should I go about making sure the module AshAuthentication.Errors.AuthenticationFailed implements AshGraphql.Error?
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.
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?
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.
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.
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.
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
vars: Map.new(error.vars), I would suggest vars: %{}, just to be on the safe side
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