Authentication actions as graphQL queries/mutations?

Eduardo B. Alexandre
2023-02-10

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.Resolverresolve 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.