The Context and Authentication
Absinthe context exists to provide shared values to a given document execution.
A common use would be to pass in the current user of a given request. The context
is set at the call to Absinthe.run
, and cannot be modified over the course of
a given execution.
Basic Usage
As a basic example let's think about a profile page, where we want the current user to be able to access basic information about themselves, but not other users.
First we'll need a very basic schema:
defmodule MyAppWeb.Schema do
use Absinthe.Schema
@fakedb %{
"1" => %{name: "Bob", email: "bubba@foo.com"},
"2" => %{name: "Fred", email: "fredmeister@foo.com"},
}
query do
field :profile, :user do
resolve fn _, _, _ ->
# How could we get a current user here?
end
end
end
object :user do
field :id, :id
field :name, :string
field :email, :string
end
end
A query we might want could look like:
{
profile {
email
}
}
If we're signed in as user 1, we should get only user 1's email, for example:
{
"profile": {
"email": "bubba@foo.com"
}
}
In order to set the context, our call to Absinthe.run/3
should look like:
Absinthe.run(document, MyAppWeb.Schema, context: %{current_user: %{id: "1"}})
To access this, we need to update our query's resolve function:
query do
field :profile, :user do
resolve fn _, _, %{context: %{current_user: current_user}} ->
{:ok, Map.get(@fakedb, current_user.id)}
end
end
end
And now it works!
Context and Plugs
When using Absinthe.Plug you don't have direct access to the Absinthe.run call.
Instead, we can use Absinthe.Plug.put_options/2
to set context values which
Absinthe.Plug will use to pass it along to Absinthe.run.
Setting up your GraphQL context is as simple as writing a plug that inserts the appropriate values into the connection.
Let's use this mechanism to set our current_user from the previous example via an authentication header. We will use the same Schema as before.
First, our plug. We'll be checking the connection for the authorization
header, and calling
out to some unspecified authentication mechanism.
defmodule MyAppWeb.Context do
@behaviour Plug
import Plug.Conn
import Ecto.Query, only: [where: 2]
alias MyApp.{Repo, User}
def init(opts), do: opts
def call(conn, _) do
context = build_context(conn)
Absinthe.Plug.put_options(conn, context: context)
end
@doc """
Return the current user context based on the authorization header
"""
def build_context(conn) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, current_user} <- authorize(token) do
%{current_user: current_user}
else
_ -> %{}
end
end
defp authorize(token) do
User
|> where(token: ^token)
|> Repo.one
|> case do
nil -> {:error, "invalid authorization token"}
user -> {:ok, user}
end
end
end
This plug will use the authorization
header to lookup the current user. If one
is found, it correctly sets the absinthe context. If you're using Guardian or
some other library that provides utilities for authenticating users you can use
those here too, and just add their output to the context.
If there is no current user it's better to simply not have the :current_user
key inside the map, instead of doing %{current_user: nil}
. This way you an
just pattern match for %{current_user: user}
in your code and not need to
worry about the nil case.
Using this plug is very simple. If we're just in a normal plug context we can just make sure it's plugged prior to Absinthe.Plug
plug MyAppWeb.Context
plug Absinthe.Plug,
schema: MyAppWeb.Schema
If you're using a Phoenix router, add the context plug to a pipeline.
defmodule MyAppWeb.Router do
use Phoenix.Router
resource "/pages", MyAppWeb.PagesController
pipeline :graphql do
plug MyAppWeb.Context
end
scope "/api" do
pipe_through :graphql
forward "/", Absinthe.Plug,
schema: MyAppWeb.Schema
end
end