Authentication actions as graphQL queries/mutations?
Eduardo B. Alexandre:
Is it possible to use AshGraphQL with AshAuthentication to sign_in and sign_up?
I’ve tried adding the
sign_in_with_password
action as a graphql query:
graphql do
type :user
queries do
get :sign_in_with_password, :sign_in_with_password
end
end
But this will ask for the user id as input, also it will not return the token, only the user.
What I wanted was to be able to do something like this:
mutation {
signInWithPassword(email: "alice@prisma.io", password: "graphql") {
token
user {
id,
...
}
}
}
ZachDaniel:
Yep! You can do that 🙂 The basic way is this:
queries do
get :sign_in_with_password, :sign_in_with_password do
identity nil # tell it not to use anything for looking up
as_mutation? true
end
end
ZachDaniel:
That should change the response type to include the token metadata
ZachDaniel:
You can also use the
modify_resolution
which takes an MFA (if I recall correctly) and you should be able to leverage that to modify the conn. <@360450161845075968> are you doing authentication over graphql? I don’t have a setup that does it currently, so I don’t recall exactly what it looks like to do that.
Eduardo B. Alexandre:
Not sure if I’m doing something wrong, but after making the above changes, I get this error in playground:

ZachDaniel:
🤔
ZachDaniel:
do you need the mutation name?
ZachDaniel:
mutation MutationName {
ZachDaniel:
like just a made up name
ZachDaniel:
Does your new mutation show up in the schema?
Eduardo B. Alexandre:
Ah, I think I found a bug
Eduardo B. Alexandre:
I need to have at least one mutation implemented in the
mutations
part of
graphql
to make my
sign_in_with_password
query shows up as a mutation.
Eduardo B. Alexandre:
So, if I do this:
graphql do
type :user
queries do
get :sign_in_with_password, :sign_in_with_password do
identity nil
as_mutation? true
end
end
end
It will not show up in playground (see image)

Eduardo B. Alexandre:
But, if I do this:
graphql do
type :user
queries do
get :sign_in_with_password, :sign_in_with_password do
identity nil
as_mutation? true
end
end
mutations do
create :bla, :register_with_password
end
end
Now it will show up fine

Eduardo B. Alexandre:
It is still requiring the
id
field though

barnabasj:
Not right now, we are just using rest endpoints for auth. Still doing it with POW, unfortunately I did not have time to change it to ash_auth as of yet
ZachDaniel:
<@816769011533742100> can I see your schema?
ZachDaniel:
the absinthe schema, I mean
ZachDaniel:
Wondering if you have an empty
mutations
block or not
Eduardo B. Alexandre:
Sure, but I’m not sure how to get it, isn’t that automatically generated by AshGraphQL during compilation time? Or do you mean the
graphql
code block inside my resource?
ZachDaniel:
Just the contents of the absinthe schema that you have currently
ZachDaniel:
like you should have a
schema.ex
that calls
use AshGraphql
ZachDaniel:
It will be mostly empty, just want to see if there are issues there
Eduardo B. Alexandre:
Ahh, got it
Eduardo B. Alexandre:
defmodule Marketplace.GraphQL.Schema do
use Absinthe.Schema
@apis [Marketplace.Markets, Marketplace.Accounts]
use AshGraphql, apis: @apis
query do
end
mutation do
end
def context(context), do: AshGraphql.add_context(context, @apis)
def plugins, do: [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
ZachDaniel:
okay that does look right. Weird that you have to have at least one mutation…
ZachDaniel:
Oh
ZachDaniel:
For the id issue, set
identity false
ZachDaniel:
Not
identity nil
sorry
Eduardo B. Alexandre:
Ah, yeah, now there is not more an id as input 😁
Eduardo B. Alexandre:
But, also the API only returns the user resource, not a token
ZachDaniel:
Are you on the latest version of ash_authentication?
ZachDaniel:
oh, you will also need to be on the latest version of
ash_graphql
as well
ZachDaniel:
returning metadata on read actions was added recently
Eduardo B. Alexandre:
let me check that
Eduardo B. Alexandre:
You are correct, I was in an outdated version of ash_authentication
Eduardo B. Alexandre:
I had to add this to my query
type_name :user_with_token
and after that now I’m getting the token 😁
ZachDaniel:
🥳
Eduardo B. Alexandre:
But I’m seeing something else that is a little bit odd.
Not sure why, but the query will onyl work if I add the hashedPassword field to be returned, if I don’t add it, the query will crash in the backend
Eduardo B. Alexandre:
This is the error that I get

ZachDaniel:
okay that looks like a bug in
ash_graphql
ZachDaniel:
well…that looks like two bugs
ZachDaniel:
but lets fix the first one, one sec
Eduardo B. Alexandre:
This is the query that will trigger this:
mutation {
signInWithPassword(email: "blibs@blobs.com", password: "12345678") {
email
}
}
This one will work just fine:
mutation {
signInWithPassword(email: "blibs@blobs.com", password: "12345678") {
hashedPassword
}
}
This only happens with the
hashedPassword
field, all other fields works just fine
ZachDaniel:
this is strange, we already fixed the issue around it not selecting
hashed_password
…
ZachDaniel:
Okay, so
ash_graphql
main has a fix for the first error
Eduardo B. Alexandre:
Let me try with it here and see how it goes
ZachDaniel:
🤔 I think something may have been lost in a merge or something here? I could swear I fixed this
Eduardo B. Alexandre:
The error changed a little bit

ZachDaniel:
Yeah, that looks like what I expected
ZachDaniel:
I will push the fix up for you
ZachDaniel:
not sure what happened there
ZachDaniel:
gimme a few
Eduardo B. Alexandre:
thanks man 😁
ZachDaniel:
You could try that branch out and see how it works for you
query-select-fixes
in
ash_authentication
Eduardo B. Alexandre:
Seems like that branch fixed the issue for me 😁
ZachDaniel:
🥳
Eduardo B. Alexandre:
Sorry to bombard you with more questions, but is there a way for me to make the register_with_password also return a token? From the documentation, it doesn’t seem like I can use the same approach I did with the query as mutation
ZachDaniel:
🤔 I thought that one should do it on its own
Eduardo B. Alexandre:
maybe I’m doing something wrong, I will post more information here
Eduardo B. Alexandre:
This is my mutation code block:
mutations do
create :register_with_password, :register_with_password
end
Eduardo B. Alexandre:
As can be seem from the schema, there is no token field being returned:

ZachDaniel:
Have you defined the
register_with_password
action yourself?
ZachDaniel:
actually, looks like that is also missing from the action definition
Eduardo B. Alexandre:
No, I’m using the default one from AshAuthentication
ZachDaniel:
can you update your branch?
ZachDaniel:
I just pushed another commit to the branch
ZachDaniel:
mix deps.update ash_authentication
Eduardo B. Alexandre:
Seems like something broke with the new commit

ZachDaniel:
oh
ZachDaniel:
sorry one sec
ZachDaniel:
okay try again 😄
Eduardo B. Alexandre:

Eduardo B. Alexandre:
Seems like it is working now 😄
ZachDaniel:
🥳
Eduardo B. Alexandre:
Hopefully, this is my last question regarding this, at least for a while 😅 , but I showed the API to my frontend team and they complained that it is not using GraphQL standards…
Instead of having this:
mutation signInWithPassword($email: String!, $password: String!, $passwordConfirmation: String!) {
user {
id
email
token
}
}
they want this:
mutation signInWithPassword ($signInInput: SignInInput!) {
token
user {
id
email
}
}
Basically having the token outside of the user, and having the inputs ins a input object the same way the registerWithPassword is.
ZachDaniel:
🤔 potentially 😄
ZachDaniel:
It would take some time to make those work
Eduardo B. Alexandre:
maybe just converting the action itself to a create would do the trick?
ZachDaniel:
Yeah, potentially.
ZachDaniel:
Try this:
# you need unique action names
create :sign_in_with_password_create do
argument :email, :string do
allow_nil? false
sensitive? true
end
argument :password, :string do
allow_nil? false
sensitive? true
end
argument :password_confirmation, :string do
allow_nil? false
sensitive? true
end
metadata :token, :string do
allow_nil? false
end
manual fn changeset, _ ->
__MODULE__
|> Ash.Query.for_read(:sign_in_with_password, changeset.arguments)
|> YourApi.read_one()
end
end
ZachDaniel:
This is a bit of a hack but would get you the api you want I believe
ZachDaniel:
Then you could do
mutations do
mutation :sign_in_with_password, :sign_in_with_password_create
end
ZachDaniel:
Once you get it working, could you make an issue on
ash_hq
documenting what you couldn’t do with a read action and what you had to do to work around it? We can add options in the future to make this better (like
input_object? true
for queries, and
metadata_placement :new_type | :alongside
Eduardo B. Alexandre:
Yes, I will do that for sure
Eduardo B. Alexandre:
Hmm, I’m getting some absinthe schema error because the
email
field identifier is not unique 🤔
ZachDaniel:
Ah, yeah
ZachDaniel:
add
accept []
to the create action
Eduardo B. Alexandre:
Thanks a lot Zach, that worked!
ZachDaniel:
🥳
ZachDaniel:
You also have the option of defining a regular old mutation in graphql
ZachDaniel:
mutations do
object :sign_in_result do
field :token :string
field :user, :user
end
field :sign_in, type: :sign_in_result do
arg :email, ...
resolve fn _parent, args, _ ->
#Use your action here,
# return this
%{
user: user,
token: user.__metadata__.token
}
end
end
end
that kind of thing
ZachDaniel:
So that can be an escape hatch when you want to do something
ash_graphql
doesn’t support.
ZachDaniel:
You can read more about it in the absinthe docs: https://hexdocs.pm/absinthe/mutations.html#next-step
ZachDaniel:
If you do it that way you won’t need the unnecessary create action
Eduardo B. Alexandre:
Actually I think that was what I was about to ask, they seem to not be happy yet with it because of something related to the function being global or whatever, I’m not sure since I just started using GraphQL.
ZachDaniel:
🤔
Eduardo B. Alexandre:
I’m starting to get pissed off with them tbh 😅
ZachDaniel:
😆
Eduardo B. Alexandre:
In case of that escape hatch, It seems that I would return a map and ash_graphql woudl create the graphql schemas correct?
ZachDaniel:
Yeah, if you use our types, you just need to return the appropriate map
ZachDaniel:
What do they mean in terms of
global
?
ZachDaniel:
Do they want your mutations split up by category or something?
Eduardo B. Alexandre:
They basically sent me this:
mutation signIn ($signInInput: SignInInput!) {
signIn (signInInput: $signInInput) {
token
user {
id
email
firstName
lastName
phoneNumber
}
}
}
That is what they expected, I mean, I think they are talking about that signIn inside a signIn I guess, I will need to read more about graphQL to get this..
ZachDaniel:
🤔 thats really just one mutation, its not nested
ZachDaniel:
its just how you name an operation
Eduardo B. Alexandre:
What they said is that they expected that I would send the input inside the query. Not sure why
ZachDaniel:
That mutation should basically for you I imagine
Eduardo B. Alexandre:
But, going back to this, can I manually create my own Absinthe schema by and use it there? I guess that way I would be able to do exactly what they want.
ZachDaniel:
Yep 🙂
ZachDaniel:
I’m still curious whats wrong with the other mutation
Eduardo B. Alexandre:
And to do that it would be using that resolve function or another one? Is there a way that I can also manually define the Absinthe inputs too? I mean, basically make the whole query by hand I guess
ZachDaniel:
Yeah, once you start writing absinthe stuff you’re basically in full control
Eduardo B. Alexandre:
I will look into that, and after I find out what exactly they are talking about I will update here so we can see if it is just meaningless complains or something that would add value to ash_graphql and possibly create a ticket in the ash_graphql repo
Eduardo B. Alexandre:
And thanks a lot for the patience Zach, I really appreciate that
ZachDaniel:
no problem 🙂 best of luck!
Eduardo B. Alexandre:
<@197905764424089601> quick question about this. what do I need to import to make the object and field available?
ZachDaniel:
Not sure really, I’d suggest reading the absinthe documentation
Eduardo B. Alexandre:
It is what they use as far as I can tell, I think it just breaks because I’m using it directly inside the
mutations
block
ZachDaniel:
I think the object part goes outside of the mutations block
Eduardo B. Alexandre:
the part it complains is actually this one:
field :sign_in, type: :sign_in_result do
ZachDaniel:
That part goes in mutations
Eduardo B. Alexandre:
From their documentation this should be inside a
mutation do
block
Eduardo B. Alexandre:
If I add it to the
mutations do
block, I get this error:
Invalid schema notation:
field
must only be used within
input_object
,
interface
,
object
,
schema_declaration
. Was used in
schema
.
ZachDaniel:
Oh…can i see the whole file?
Eduardo B. Alexandre:
ZachDaniel:
oohhhhh
ZachDaniel:
all that stuff goes in your
schema.ex
file
ZachDaniel:
not in the resource
Eduardo B. Alexandre:
Yep, that was it hahah, now it works
Eduardo B. Alexandre:
Ok, the last question of today, I promise.
Do we have some documentation or can you tell me where in the ash code you handle
Ash.Error.Forbidden
errors for mutations? I would like to add that to my custom mutation so it handles errors the same way
ZachDaniel:
I’ll add a helper for you
ZachDaniel:
Okay
ZachDaniel:
In the
main
branch of
ash_graphql
ZachDaniel:
there is
AshGraphql.Error.to_errors(errors)
ZachDaniel:
So you can do something like this:
do_action
|> case do
{:ok, result} ->
...
{:error, error} ->
%{errors: AshGraphql.Error.to_errors(error)}
end
ZachDaniel:
Haven’t tried it myself but I think something like that should work
Eduardo B. Alexandre:
Hmm, I don’t think that will work since that protocol doesn’t seem to be implemented for Ash.Error.Forbidden which is the error that the sign_in_with_password will return
Eduardo B. Alexandre:
This is the full error btw
ZachDaniel:
You can implement the protocol yourself for:
AshAuthentication.Errors.AuthenticationFailed
ZachDaniel:
and make it return an error like “invalid username or password”
Eduardo B. Alexandre:
Ah, got it, I will that. I just though that was already implemented somewhere in AshGraphql
Eduardo B. Alexandre:
Since it already handles the error correctly when using it
ZachDaniel:
🤔 what do you mean?
Eduardo B. Alexandre:
I mean the MutationError which are added and handled automatically by AshGraphql when creating mutations with it

ZachDaniel:
I’m pretty sure that error won’t actually show up though
ZachDaniel:
because it doesn’t have the protocol implemented for it
ZachDaniel:
AshGraphql won’t show errors it doesn’t know how to display.
Eduardo B. Alexandre:
Yeah, it will show up as a generic error
ZachDaniel:
gotcha, okay
ZachDaniel:
like “something went wrong”?
Eduardo B. Alexandre:
Yeah, just noticed that 😅
ZachDaniel:
You should have what you need by either implementing the protocol for that error or doing some custom poking at the errors and returning whatever error you want 🙂
Eduardo B. Alexandre:
Yep, working great now!

ZachDaniel:
So eventually we should add options for read actions to return result types like mutations, and to accept input objects like mutations.
Eduardo B. Alexandre:
I was about to create another post, but I think this is kinda on topic…
Since I can now get the token from my sign-in query, I went ahead and added it to the HTTP header as a bearer authentication header and I can see that when I run another query in GraphQL, the route pipeline will fetch the token, find the user, and run the AshGraphQL plug which adds the user as an actor to the Absinthe context.
Looking at the documentation, this seems like it is all that is needed to make the actor available in my resource.
But this doesn’t seem to be working, I added some log to
AshGraphql.Graphql.Resolver
resolve
function and I can see that when the code tries to fetch the actor, it returns
nil
.
Eduardo B. Alexandre:
I can elaborate more on what changes I made, but I basically followed the https://ash-hq.org/docs/guides/ash_graphql/latest/how_to/authorize-with-graphql guide
Eduardo B. Alexandre:
Ah, I think I found the issue
Eduardo B. Alexandre:
For reference.
The problema is that I was using
:load_from_bearer
in my pipeline which will add the user in the connection assigns with the
:current_user
key.
Since I wanted to still use that plug, I just created a small plug that will set that assign as the actor (see image)

Eduardo B. Alexandre:
So, in case someone needs to do something similar in the future, this is how I did my customs mutations with graphql plus Ash:
First, in my
Marketplace.Accounts.User
resource, I added the graphql code block:
graphql do
type :user
end
Eduardo B. Alexandre:
Then, I created a module to store my custom queries/mutations/types:
defmodule Marketplace.Accounts.User.GraphQL do
@moduledoc false
alias Marketplace.Accounts.User
use Absinthe.Schema.Notation
input_object :sign_in_with_password_input do
field :email, non_null(:string)
field :password, non_null(:string)
end
object :sign_in_with_password_result do
field :token, :string
field :user, :user
field :errors, list_of(:mutation_error)
end
input_object :register_with_password_input do
field :email, non_null(:string)
field :password, non_null(:string)
field :password_confirmation, non_null(:string)
end
object :register_with_password_result do
field :token, :string
field :user, :user
field :errors, list_of(:mutation_error)
end
object :accounts_user_mutations do
field :sign_in_with_password, type: :sign_in_with_password_result do
arg :input, non_null(:sign_in_with_password_input)
resolve(fn _, %{input: args}, _ ->
with {:ok, user} <- User.sign_in_with_password(args) do
{:ok, %{user: user, token: user.__metadata__.token}}
else
{:error, _} ->
{:ok, %{errors: [%{code: "invalid_credentials"}]}}
end
end)
end
field :register_with_password, type: :register_with_password_result do
arg :input, non_null(:register_with_password_input)
resolve(fn _, %{input: args}, _ ->
with {:ok, user} <- User.register_with_password(args) do
{:ok, %{user: user, token: user.__metadata__.token}}
else
{:error, %{errors: errors}} ->
errors = Enum.map(errors, &AshGraphql.Error.to_error/1)
{:ok, %{errors: errors}}
end
end)
end
end
end
As you can see, right now this has 2 mutations,
sign_in_with_password
and
register_with_password
.
Eduardo B. Alexandre:
Now inside my graphql schema, I added:
import_types Marketplace.Accounts.User.GraphQL
mutation do
import_fields :accounts_user_mutations
end
And that is pretty much it. I’m happy with this solution, but any suggestions are welcome.