Authentication with AshGraphQL for specific queries/mutations
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:
You would do something like this: https://hexdocs.pm/absinthe/Absinthe.Middleware.html#module-object-wide-authentication
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