Absinthe: Remove a default middleware on specific resolvers

Reading through the Absinthe Graphql book, written by the library’s author, you can add middleware to ALL routes, or all mutations or all queries:

  def middleware(mw, _field, _obj) do
    mw ++ [Authentication]
  end

This adds the authentication middleware to every single field and object requested in GQL.

The question is, is there a way to keep this as the default, but REMOVE it in some scenarios? (i.e. a registration mutation).

Something like (this doesn’t work as I would hope):

  # Don't apply Authentication middlewear to auth_mutations
  def middleware(mw, _field, %{identifier: :auth_mutations}) do
    mw
  end

  # All graphql routes require authentication
  def middleware(mw, _field, _obj) do
    mw ++ [Authentication]
  end
1 Like

This is a very old thread, but it’s the a top Google search result for this so I figured I’d drop an answer here (apologies if this was not desired!):

I ran into this same issue and eventually realized you can check the identifier of the field. For instance:

  def middleware(middleware, field, obj) do
    if obj.identifier in [:query, :subscription, :mutation] &&
         field.identifier not in [:authenticate, :refresh] do
      [CrystifiWeb.Schema.Middleware.RequireAuth | middleware]
    else
      middleware
    end
  end

This will not authenticate the :authenticate and :refresh mutations. (This actually won’t authenticate anything named :authenticate and :refresh, but the reasons are pretty evident, and I admittedly didn’t feel like fixing up this example code…)

4 Likes

It’s appreciated! I ended up just doing a basic json endpoint to handle non auth stuff, but I’d love to dive back in and see how this works!

1 Like

Another possibility might be to still apply the middleware everywhere but have that middleware check a field’s metadata to determine if auth should be disabled. For example, an auth-less field could have meta :no_auth, true in the schema you define. That “meta” macro is built into absinthe.

It could have the advantage of not hard coding the exception list in your middleware, plus you get to express the “don’t auth this” instruction in your schema itself which might make it easier to understand later.

6 Likes

This actually looks perfect, slightly surprised the docs didn’t mention it (a Google search actually shows this very topic on the first page of results).

As a fun extra bit with this: If you set it via meta you can actually look for that meta flag in the def middleware/3 function, which will then avoid even the runtime overhead.

2 Likes

I’m trying to bypass a middleware ensuring authentication, but I can’t make it work, I suspect because some magic happens with middleware, maybe. It’s the weirdest thing:

The first pain point is that there’s no easy way to get the meta.
I tried the method suggested in the docs:

type = Absinthe.Schema.lookup_type(MyAppWeb.GraphQL.Schema, :register_user)
Absinthe.Type.meta(type, :private)

But then it complains that the schema is not yet available:

= Compilation error in file lib/my_app_web/graphql/schema.ex ==
** (UndefinedFunctionError) function MyAppWeb.GraphQL.Schema.Compiled.absinthe_type/1 is undefined (module MyAppWeb.GraphQL.Schema.Compiled is not available)

The middleware are defined in the Schema (middleware/3 callback), just as in the book. I guess the function call does some introspection and the Schema has not finished compiling as the function is called in the schema itself.

So I added this instead:

skip_authentication? =
  field.__private__
  |> Keyword.fetch!(:meta)
  |> Keyword.get(:skip_authentication, false)

Seems ugly to access __private__ but I couldn’t find any better way. However this still doesn’t work because unfortunately the meta key is not added unless there’s a meta. It’s a pity that it is not always added, and if no metadata present, have an empty list for meta. As I need a conditional on the presence of meta, I went for this function:

defp get_meta(%{__private__: private}, key, default) do
  private
  |> Keyword.get(:meta, [])
  |> Keyword.get(key, default)
end

Now the problem:

Even if I bypass the Authenticate middleware through a conditional in the middleware/3 callback, it will still execute. I’ve searched for hours but can’t figure out why the middleware would execute. Hence I suspect some magic is happening, Here is the code inside middleware/3:

def middleware(middleware, field, object) do
  skip_authentication? = get_meta(field, :skip_authentication, false)

  middleware =
    if skip_authentication? do
      middleware
    else
      [Middleware.Authenticate | middleware]
    end

  case object do
    %{identifier: :mutation} ->
      middleware ++ [Middleware.HandleChangesetErrors, Middleware.HandleNotFoundErrors]

    _ ->
      middleware
  end
end

But surprisingly the middleware is still called. Tried logging for hours trying to understand why, but really can’t figure this one out.

For the last point I had to change this

object :session do
  meta :skip_authentication, true
  field :token, :string
end

to this

object :session do
  field :token, :string do
    meta :skip_authentication, true
  end
end

The meta should be added to the mutation field but also to every field returned in the response payload. I wonder if there’s an easier way. It’s not possible to add it at the object level for every field, the meta has to be repeated for every field.