AE
Ash Elixirβ€’3y ago
Blibs

Authentication actions as graphQL queries/mutations?

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
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: "[email protected]", password: "graphql") {
token
user {
id,
...
}
}
}
mutation {
signInWithPassword(email: "[email protected]", password: "graphql") {
token
user {
id,
...
}
}
}
95 Replies
ZachDaniel
ZachDanielβ€’3y ago
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
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
That should change the response type to include the token metadata 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. @barnabasj 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.
Blibs
BlibsOPβ€’3y ago
Not sure if I'm doing something wrong, but after making the above changes, I get this error in playground:
No description
ZachDaniel
ZachDanielβ€’3y ago
πŸ€” do you need the mutation name? mutation MutationName { like just a made up name Does your new mutation show up in the schema?
Blibs
BlibsOPβ€’3y ago
Ah, I think I found a bug 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.
Blibs
BlibsOPβ€’3y ago
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
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)
No description
Blibs
BlibsOPβ€’3y ago
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
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
No description
Blibs
BlibsOPβ€’3y ago
It is still requiring the id field though
No description
barnabasj
barnabasjβ€’3y ago
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
ZachDanielβ€’3y ago
@Blibs can I see your schema? the absinthe schema, I mean Wondering if you have an empty mutations block or not
Blibs
BlibsOPβ€’3y ago
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
ZachDanielβ€’3y ago
Just the contents of the absinthe schema that you have currently like you should have a schema.ex that calls use AshGraphql It will be mostly empty, just want to see if there are issues there
Blibs
BlibsOPβ€’3y ago
Ahh, got it
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
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
ZachDanielβ€’3y ago
okay that does look right. Weird that you have to have at least one mutation... Oh For the id issue, set identity false Not identity nil sorry
Blibs
BlibsOPβ€’3y ago
Ah, yeah, now there is not more an id as input 😁 But, also the API only returns the user resource, not a token
ZachDaniel
ZachDanielβ€’3y ago
Are you on the latest version of ash_authentication? oh, you will also need to be on the latest version of ash_graphql as well returning metadata on read actions was added recently
Blibs
BlibsOPβ€’3y ago
let me check that You are correct, I was in an outdated version of ash_authentication I had to add this to my query type_name :user_with_token and after that now I'm getting the token 😁
ZachDaniel
ZachDanielβ€’3y ago
πŸ₯³
Blibs
BlibsOPβ€’3y ago
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
Blibs
BlibsOPβ€’3y ago
This is the error that I get
No description
ZachDaniel
ZachDanielβ€’3y ago
okay that looks like a bug in ash_graphql well...that looks like two bugs but lets fix the first one, one sec
Blibs
BlibsOPβ€’3y ago
This is the query that will trigger this:
mutation {
signInWithPassword(email: "[email protected]", password: "12345678") {
email
}
}
mutation {
signInWithPassword(email: "[email protected]", password: "12345678") {
email
}
}
This one will work just fine:
mutation {
signInWithPassword(email: "[email protected]", password: "12345678") {
hashedPassword
}
}
mutation {
signInWithPassword(email: "[email protected]", password: "12345678") {
hashedPassword
}
}
This only happens with the hashedPassword field, all other fields works just fine
ZachDaniel
ZachDanielβ€’3y ago
this is strange, we already fixed the issue around it not selecting hashed_password... Okay, so ash_graphql main has a fix for the first error
Blibs
BlibsOPβ€’3y ago
Let me try with it here and see how it goes
ZachDaniel
ZachDanielβ€’3y ago
πŸ€” I think something may have been lost in a merge or something here? I could swear I fixed this
Blibs
BlibsOPβ€’3y ago
The error changed a little bit
No description
ZachDaniel
ZachDanielβ€’3y ago
Yeah, that looks like what I expected I will push the fix up for you not sure what happened there gimme a few
Blibs
BlibsOPβ€’3y ago
thanks man 😁
ZachDaniel
ZachDanielβ€’3y ago
You could try that branch out and see how it works for you query-select-fixes in ash_authentication
Blibs
BlibsOPβ€’3y ago
Seems like that branch fixed the issue for me 😁
ZachDaniel
ZachDanielβ€’3y ago
πŸ₯³
Blibs
BlibsOPβ€’3y ago
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
ZachDanielβ€’3y ago
πŸ€” I thought that one should do it on its own
Blibs
BlibsOPβ€’3y ago
maybe I'm doing something wrong, I will post more information here This is my mutation code block:
mutations do
create :register_with_password, :register_with_password
end
mutations do
create :register_with_password, :register_with_password
end
Blibs
BlibsOPβ€’3y ago
As can be seem from the schema, there is no token field being returned:
No description
ZachDaniel
ZachDanielβ€’3y ago
Have you defined the register_with_password action yourself? actually, looks like that is also missing from the action definition
Blibs
BlibsOPβ€’3y ago
No, I'm using the default one from AshAuthentication
ZachDaniel
ZachDanielβ€’3y ago
can you update your branch? I just pushed another commit to the branch mix deps.update ash_authentication
Blibs
BlibsOPβ€’3y ago
Seems like something broke with the new commit
No description
ZachDaniel
ZachDanielβ€’3y ago
oh sorry one sec okay try again πŸ˜„
Blibs
BlibsOPβ€’3y ago
No description
Blibs
BlibsOPβ€’3y ago
Seems like it is working now πŸ˜„
ZachDaniel
ZachDanielβ€’3y ago
πŸ₯³
Blibs
BlibsOPβ€’3y ago
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
}
}
mutation signInWithPassword($email: String!, $password: String!, $passwordConfirmation: String!) {
user {
id
email
token
}
}
they want this:
mutation signInWithPassword ($signInInput: SignInInput!) {
token
user {
id
email
}
}
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
ZachDanielβ€’3y ago
πŸ€” potentially πŸ˜„ It would take some time to make those work
Blibs
BlibsOPβ€’3y ago
maybe just converting the action itself to a create would do the trick?
ZachDaniel
ZachDanielβ€’3y ago
Yeah, potentially. 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
# 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
This is a bit of a hack but would get you the api you want I believe Then you could do
mutations do
mutation :sign_in_with_password, :sign_in_with_password_create
end
mutations do
mutation :sign_in_with_password, :sign_in_with_password_create
end
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
Blibs
BlibsOPβ€’3y ago
Yes, I will do that for sure Hmm, I'm getting some absinthe schema error because the email field identifier is not unique πŸ€”
ZachDaniel
ZachDanielβ€’3y ago
Ah, yeah add accept [] to the create action
Blibs
BlibsOPβ€’3y ago
Thanks a lot Zach, that worked!
ZachDaniel
ZachDanielβ€’3y ago
πŸ₯³ You also have the option of defining a regular old mutation in graphql
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
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 So that can be an escape hatch when you want to do something ash_graphql doesn't support. You can read more about it in the absinthe docs: https://hexdocs.pm/absinthe/mutations.html#next-step If you do it that way you won't need the unnecessary create action
Blibs
BlibsOPβ€’3y ago
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
ZachDanielβ€’3y ago
πŸ€”
Blibs
BlibsOPβ€’3y ago
I'm starting to get pissed off with them tbh πŸ˜…
ZachDaniel
ZachDanielβ€’3y ago
πŸ˜†
Blibs
BlibsOPβ€’3y ago
In case of that escape hatch, It seems that I would return a map and ash_graphql woudl create the graphql schemas correct?
ZachDaniel
ZachDanielβ€’3y ago
Yeah, if you use our types, you just need to return the appropriate map What do they mean in terms of global? Do they want your mutations split up by category or something?
Blibs
BlibsOPβ€’3y ago
They basically sent me this:
mutation signIn ($signInInput: SignInInput!) {
signIn (signInInput: $signInInput) {
token
user {
id
email
firstName
lastName
phoneNumber
}
}
}
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
ZachDanielβ€’3y ago
πŸ€” thats really just one mutation, its not nested its just how you name an operation
Blibs
BlibsOPβ€’3y ago
What they said is that they expected that I would send the input inside the query. Not sure why
ZachDaniel
ZachDanielβ€’3y ago
That mutation should basically for you I imagine
Blibs
BlibsOPβ€’3y ago
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
ZachDanielβ€’3y ago
Yep πŸ™‚ I'm still curious whats wrong with the other mutation
Blibs
BlibsOPβ€’3y ago
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
ZachDanielβ€’3y ago
Yeah, once you start writing absinthe stuff you're basically in full control
Blibs
BlibsOPβ€’3y ago
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 And thanks a lot for the patience Zach, I really appreciate that
ZachDaniel
ZachDanielβ€’3y ago
no problem πŸ™‚ best of luck!
Blibs
BlibsOPβ€’3y ago
@Zach Daniel quick question about this. what do I need to import to make the object and field available?
ZachDaniel
ZachDanielβ€’3y ago
Not sure really, I’d suggest reading the absinthe documentation
Blibs
BlibsOPβ€’3y ago
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
ZachDanielβ€’3y ago
I think the object part goes outside of the mutations block
Blibs
BlibsOPβ€’3y ago
the part it complains is actually this one: field :sign_in, type: :sign_in_result do
ZachDaniel
ZachDanielβ€’3y ago
That part goes in mutations
Blibs
BlibsOPβ€’3y ago
From their documentation this should be inside a mutation do block 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
ZachDanielβ€’3y ago
Oh…can i see the whole file?
Blibs
BlibsOPβ€’3y ago
ZachDaniel
ZachDanielβ€’3y ago
oohhhhh all that stuff goes in your schema.ex file not in the resource
Blibs
BlibsOPβ€’3y ago
Yep, that was it hahah, now it works 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
ZachDanielβ€’3y ago
I'll add a helper for you Okay In the main branch of ash_graphql there is AshGraphql.Error.to_errors(errors) So you can do something like this:
do_action
|> case do
{:ok, result} ->
...
{:error, error} ->
%{errors: AshGraphql.Error.to_errors(error)}
end
do_action
|> case do
{:ok, result} ->
...
{:error, error} ->
%{errors: AshGraphql.Error.to_errors(error)}
end
Haven't tried it myself but I think something like that should work
Blibs
BlibsOPβ€’3y ago
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
Blibs
BlibsOPβ€’3y ago
This is the full error btw
ZachDaniel
ZachDanielβ€’3y ago
You can implement the protocol yourself for: AshAuthentication.Errors.AuthenticationFailed and make it return an error like "invalid username or password"
Blibs
BlibsOPβ€’3y ago
Ah, got it, I will that. I just though that was already implemented somewhere in AshGraphql Since it already handles the error correctly when using it
ZachDaniel
ZachDanielβ€’3y ago
πŸ€” what do you mean?
Blibs
BlibsOPβ€’3y ago
I mean the MutationError which are added and handled automatically by AshGraphql when creating mutations with it
No description
ZachDaniel
ZachDanielβ€’3y ago
I'm pretty sure that error won't actually show up though because it doesn't have the protocol implemented for it AshGraphql won't show errors it doesn't know how to display.
Blibs
BlibsOPβ€’3y ago
Yeah, it will show up as a generic error
ZachDaniel
ZachDanielβ€’3y ago
gotcha, okay like "something went wrong"?
Blibs
BlibsOPβ€’3y ago
Yeah, just noticed that πŸ˜…
ZachDaniel
ZachDanielβ€’3y ago
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 πŸ™‚
Blibs
BlibsOPβ€’3y ago
Yep, working great now!
No description
ZachDaniel
ZachDanielβ€’3y ago
So eventually we should add options for read actions to return result types like mutations, and to accept input objects like mutations.
Blibs
BlibsOPβ€’3y ago
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. 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 Ah, I think I found the issue
Blibs
BlibsOPβ€’3y ago
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)
No description
Blibs
BlibsOPβ€’3y ago
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
graphql do
type :user
end
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
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. Now inside my graphql schema, I added:
import_types Marketplace.Accounts.User.GraphQL

mutation do
import_fields :accounts_user_mutations
end
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.

Did you find this page helpful?