Plug is not the only interface with the middleware layer. Absinthe also comes with a middleware layer of its own and we can use it similarly to implement authorization for queries, mutations, and subscriptions.
If we want to authorize our GraphQL API and don’t want to do that within business logic (for various reasons), we can write a middleware that will handle it. I wrote about using Plug for authorization, but using a plain Plug wouldn’t work in this case.
Since GraphQL is essentially a graph, we need to cover authorization for all nested queries. Absinthe middleware can do that for us, and might also be used for specific query fields, which is quite handy.
In the following example, I’ll use Absinthe middleware to authorize queries, mutations, and subscriptions (actions) similarly as I did with the Plug. I assume roles with specific rights having access to specific actions. For example, they might be hardcoded or fetched from a data source.
Here’s the simple AuthorizationMiddleware
module implementing the Absinthe.Middleware
behaviour:
defmodule MyWeb.AuthorizationMiddleware do
@behaviour Absinthe.Middleware
def call(resolution, _config) do
action = resolution.definition.name |> Macro.underscore()
required_rights = required_action_rights[action]
if required_rights do
case has_rights(resolution, required_rights) do
false ->
resolution
|> unauthorized
true ->
resolution
end
else
resolution
|> unauthorized
end
end
defp unauthorized(resolution) do
resolution
|> Absinthe.Resolution.put_result({:error, "unauthorized"})
end
defp get_rights_from_token(resolution) do
case resolution do
%{context: context} ->
%{guardian_default_claims: claims} = context
case claims do
%{"rights" => rights} ->
rights
_ ->
[]
end
_ ->
[]
end
end
defp has_rights(resolution, required) when is_list(required) do
rights_in_token =
get_rights_from_token(resolution)
|> Enum.map(&String.to_atom(&1))
required
|> Enum.all?(&Enum.member?(rights_in_token, &1))
end
# Map query, mutation, and subscription names
# to required rights
defp required_action_rights do
%{
"users" => [:list_users]
}
end
end
The idea is to get the action (such as query) name from the resolution struct and compare it with the user’s access rights (they might be hardcoded or dynamic). The action names are available as resolution.definition.name
, and chances are you already have the authenticated user details in the resolution. In my example, I am taking them from the Guardian token. That’s how we can implement the has_rights/1
function.
You can see that most of the module are our logic as Absinthe will take care of the rest.
As with Plug, we have to plug the middleware somewhere. Since we are talking about Absinthe, it will live in the schema:
defmodule MyWeb.Schema do
...
query do
field :hello, :string do
middleware MyWeb.AuthorizationMiddleware
resolve &get_the_string/2
end
end
end
We could call it before or after the resolve/1
macro. Since it’s authorization, it goes before!
But because we don’t want to go and insert it in every relevant action, we can use the middleware/3
callback to do it all at once:
defmodule MyWeb.Schema do
...
def middleware(middleware, _field, %Absinthe.Type.Object{identifier: identifier})
when identifier in [:query, :subscription, :mutation] do
[MyWeb.AuthorizationMiddleware | middleware]
end
def middleware(middleware, _field, _object) do
middleware
end
end
Note the _field
that I don’t care about but could be used to catch the right field.
Finally, a basic test could look like the following:
defmodule MyWeb.AuthorizationMiddlewareTest do
use ExUnit.Case
use MyWeb.ConnCase
alias MyWeb.Repo
alias MyWeb.Right
alias MyWeb.AuthorizationMiddleware
describe "call/2" do
test "does allow action" do
right = Repo.get_by(Right, name: "list_users")
resolution = %Absinthe.Resolution{
context: %{
guardian_default_claims: %{"rights" => [right.name]}
},
definition: %{
name: "users"
}
}
resolution = AuthorizationMiddleware.call(resolution, nil)
assert resolution.errors == []
end
test "does not allow action" do
admin_users = Repo.get_by(Right, name: "list_users")
resolution = %Absinthe.Resolution{
context: %{
guardian_default_claims: %{"rights" => []}
},
definition: %{
name: "users"
}
}
resolution = AuthorizationMiddleware.call(resolution, nil)
assert resolution.errors == ["unauthorized"]
end
end
end
And that’s it. Remember, you can extend it to specific fields if you need to.