Authentication with AshGraphQL for specific queries/mutations

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.
22 Replies
ZachDaniel
ZachDaniel3y ago
Is this for your hand-written graphql queries or for the stuff produced by ash_graphql?
Blibs
BlibsOP3y ago
produced by ash_graphql
ZachDaniel
ZachDaniel3y ago
There is a builtin actor_present/0 check that you can use 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
policy always() do
authorize_if actor_present()
end
Blibs
BlibsOP3y ago
Ah, got it, so creating a policy to it is the correct approach 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
ZachDaniel3y ago
🤔 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 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
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
And of course you could define those changes/preparations as a module Lemme look into what adding custom policy failure reasons would look like. 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)
Blibs
BlibsOP3y ago
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
ZachDaniel3y ago
https://hexdocs.pm/absinthe/Absinthe.Middleware.html You can add an absinthe middleware that requires authentication except on those mutations 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.
Blibs
BlibsOP3y ago
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
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
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": []
}
]
}
{
"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
ZachDaniel3y ago
👍 It's on my list to look at adding special error messages for policies failing.
Blibs
BlibsOP3y ago
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
list :list_valid_properties, :read do
middleware MyMiddleware
end
ZachDaniel
ZachDaniel3y ago
You can't add middleware in ash_graphql's configuration You would do something like this: https://hexdocs.pm/absinthe/Absinthe.Middleware.html#module-object-wide-authentication 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
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
Blibs
BlibsOP3y ago
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
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. @Zach Daniel 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 I believe this is the middleware: AshGraphql.Graphql.Resolver
ZachDaniel
ZachDaniel3y ago
Interesting...I wonder if there is a halt feature in absinthe resolution
Blibs
BlibsOP3y ago
Seems like the correct approach is to correctly pattern match in all middlewares 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,
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 Can you push that change to AshGraphql?
ZachDaniel
ZachDaniel3y ago
Yes, will do. Do you want to make that PR? Its your idea, don't want to take credit for your fix happy to do it if you'd rather not though
Blibs
BlibsOP3y ago
Ah, I don't care about credits, just having this working is enough for me 😂
ZachDaniel
ZachDaniel3y ago
Sounds good, will push it up
Blibs
BlibsOP3y ago
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 Should I use master branch for now until a new version is out?
ZachDaniel
ZachDaniel3y ago
I just published a new version
Blibs
BlibsOP3y ago
Amazing! Thanks
barnabasj
barnabasj3y ago
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.
Blibs
BlibsOP3y ago
Yes, only when an actor is not defined

Did you find this page helpful?