Authentication with AshGraphQL for specific queries/mutations

Eduardo B. Alexandre
2023-02-20

Eduardo B. Alexandre:

Maybe I’m missing something, but I’m sure what is the correct approach to check and halt a GraphQL query/mutation inside AshGraphQL.

Right now I already have access to my actor, but there is no verification if the actor is really there or if it is nil .

What I thought about doing is just add a policy to check if the actor is nil or not to the queries/mutations that need to be logged, but I’m not sure if that is the right approach or if there is a better/correct way.

ZachDaniel:

Is this for your hand-written graphql queries or for the stuff produced by ash_graphql?

Eduardo B. Alexandre:

produced by ash_graphql

ZachDaniel:

There is a builtin actor_present/0 check that you can use

ZachDaniel:

if you want a resource to only be usable by logged in users always, for example, you can say

policy always() do
  authorize_if actor_present()
end

Eduardo B. Alexandre:

Ah, got it, so creating a policy to it is the correct approach

Eduardo B. Alexandre:

Hmm, one problem with that is that I can’t easily differentiate an API not being authorized because of some other policy or because the user is not logged in or the token expired…

ZachDaniel:

🤔 Yeah, good point. I’ve been wanting to make it so that policies can fail with a “reason” so you can bubble information like that up

ZachDaniel:

I still think it ought to be done at the resource level though. If you want to distinguish for now, you could do something like this:

preparations do
  prepare fn query, context -> 
    if context[:actor] do
      query
    else
      Ash.Query.add_error(query, "your custom error")
    end
  end
end

changes do
  change fn changeset, context -> 
    if context[:actor] do
      changeset
    else
      Ash.Changeset.add_error(changeset, "your custom error")
    end
  end, on: [:create, :update, :destroy] # <- this is important, global changes don't happen on destroys by default
end

ZachDaniel:

And of course you could define those changes/preparations as a module

ZachDaniel:

Lemme look into what adding custom policy failure reasons would look like.

ZachDaniel:

Might be good to see how others have dealt with this using ash_graphql (perhaps their entire graphql api requires users to be authenticated, at which point you can just do that with a plug)

Eduardo B. Alexandre:

Yeah, that would be a workaround, but my apis to login/signup are also in graphql so that would not work in my case 😅

ZachDaniel:

You can add an absinthe middleware that requires authentication except on those mutations

ZachDaniel:

I feel like you’ve had more than a few things that have pointed out some improvements we can make to ash_graphql , but those improvements all seem doable so hopefully we can get you to a point where you don’t need as much escape hatch/funky stuff.

Eduardo B. Alexandre:

I will take a look at that, thanks!

In the meantime, this is working for me:

  preparations do
    prepare fn query, context ->
      if context[:actor] do
        query
      else
        Ash.Query.add_error(query, %Ash.Error.Forbidden{errors: [%Ash.Error.Forbidden.ApiRequiresActor{}]})
      end
    end
  end

  changes do
    change fn changeset, context ->
      if context[:actor] do
        changeset
      else
        Ash.Changeset.add_error(changeset, %Ash.Error.Forbidden{errors: [%Ash.Error.Forbidden.ApiRequiresActor{}]})
      end
    end, on: [:create, :update, :destroy]

    change Changes.AddOrganizationFromActor, where: [action_is(:create)]
  end

And

defimpl AshGraphql.Error, for: Ash.Error.Forbidden.ApiRequiresActor do
  def to_error(error) do
     %{
       message: "api requires actor",
       short_message: "requires actor",
       fields: %{},
       vars: error.vars,
       code: Ash.ErrorKind.code(error)
     }
   end
end

It will result in this error:

{
  "data": {
    "listValidProperties": null
  },
  "errors": [
    {
      "code": "actor_required_by_api",
      "fields": {},
      "locations": [
        {
          "column": 3,
          "line": 2
        }
      ],
      "message": "api requires actor",
      "path": [
        "listValidProperties"
      ],
      "short_message": "requires actor",
      "vars": []
    }
  ]
}

ZachDaniel:

👍 It’s on my list to look at adding special error messages for policies failing.

Eduardo B. Alexandre:

So, I was looking at the middleware documentation, seems to me like that is the correct approach, I can see how I can add it to my custom queries/mutations, but I’m not so sure how to “inject” them in queries/mutations created directly from AshGraphql.

Looking at ash graphql code, seems like there is a middleware option when creating queries and mutations https://github.com/ash-project/ash_graphql/blob/58eb725802d4eb3dbcf8630c636bcbb0cd972e4c/lib/resource/resource.ex#L409

So I guess it would be something like this?

 list :list_valid_properties, :read do
   middleware MyMiddleware
  end

ZachDaniel:

You can’t add middleware in ash_graphql’s configuration

ZachDaniel:

Something like this:

  def middleware(middleware, field, obj) do
    if obj.identifier in [:query, :subscription, :mutation] &&
         field.identifier not in [:your, :stuff, :that, :doesnt, :require, :an, :actor] do
      [CrystifiWeb.Schema.Middleware.RequireAuth | middleware]
    else
      middleware
    end
  end

Eduardo B. Alexandre:

Yeah, I created this middleware

defmodule EnsureAuthenticated do
  @behaviour Absinthe.Middleware

  def call(resolution, _config) do
    case resolution.context.actor do
      nil ->
        IO.puts("GOT HERE!!")
        Absinthe.Resolution.put_result(resolution, {:error, "unauthenticated"})
      _ ->
        resolution
    end
  end
end

And I can see that it is being called and I can see the GOT HERE!! message in the terminal.

The problem is that even though I set the result with an error, the action is still being called (since it will fail in the action policies step).

I’m trying to understand why this is that since I believe the execution should have stopped after the put_result call.

Eduardo B. Alexandre:

<@197905764424089601> Do you know if the policies step is done via a middleware in AshGraphql?

Seems like all middleware will run anyway, so I believe that’s why it is reaching the policies

Eduardo B. Alexandre:

I believe this is the middleware: AshGraphql.Graphql.Resolver

ZachDaniel:

Interesting…I wonder if there is a halt feature in absinthe resolution

Eduardo B. Alexandre:

Seems like the correct approach is to correctly pattern match in all middlewares

Eduardo B. Alexandre:

For example, I changed the Resolver middleware like this:

defmodule AshGraphql.Graphql.Resolver do
  @moduledoc false

  require Ash.Query
  require Logger
  import AshGraphql.TraceHelpers

  def resolve(%Absinthe.Resolution{state: :resolved} = resolution, _),
    do: resolution

  def resolve(
        %{arguments: arguments, context: context} = resolution,
        {api, resource,

Now it works fine

Eduardo B. Alexandre:

Can you push that change to AshGraphql?

ZachDaniel:

Yes, will do. Do you want to make that PR? Its your idea, don’t want to take credit for your fix

ZachDaniel:

happy to do it if you’d rather not though

Eduardo B. Alexandre:

Ah, I don’t care about credits, just having this working is enough for me 😂

ZachDaniel:

Sounds good, will push it up

Eduardo B. Alexandre:

Btw, this is the topic where the author is saying that this is the correct approach https://elixirforum.com/t/how-to-stop-a-field-resolution-after-a-middleware-returns-an-error/24388/8

Eduardo B. Alexandre:

Should I use master branch for now until a new version is out?

ZachDaniel:

I just published a new version

Eduardo B. Alexandre:

Amazing! Thanks

barnabasj:

You only need this functionality to send a different error message if the actor is not defined? Everything else could be done with just policies right? Just curious because we are doing everything with policies right now. And I want to be sure I did not misunderstand something and expose something by accident.

Eduardo B. Alexandre:

Yes, only when an actor is not defined