Using the Execution Context

Now that we have a way to identify users, we need to figure out how to integrate this information with the processing of GraphQL requests so that fields that need to be secured have access to the relevant data.

One option would be to just make each field resolution function responsible for authenticating the user and have the token passed as normal arguments, but this causes two problems. If we need this information in several fields, we require the user to pass in the token in many places—not a very nice API for clients. It wouldn’t be very nice for the server, either; it would need to look up the same user in each field even though the information returned would be the same each time.

The Absinthe feature that addresses this problem is called the execution context. It’s a place where we can set values that will be available to all of our resolvers.

Handily, the final argument passed to the resolver function is an Absinthe.Resolution struct, which includes the context. Here’s a basic example of using a context and how the context is provided to Absinthe:

 defmodule​ ContextExample.Schema ​do
 use​ Absinthe.Schema
 
  query ​do
  field ​:greeting​, ​:string​ ​do
  resolve ​fn​ _, _, %{​context:​ context} ->
  {​:ok​, ​"​​Welcome ​​#{​context.current_user.name​}​​"​}
 end
 end
 end
 end
 
 # Our document
 doc = ​"​​{ greeting }"
 
 # Our context
 context = %{​current_user:​ %{​name:​ ​"​​Alicia"​}}
 
 # Running Absinthe manually
 Absinthe.run(doc, ContextExample.Schema, ​context:​ context)

If you paste this into iex -S mix, you’ll see this result:

 {​:ok​, %{​data:​ %{​"​​greeting"​ => ​"​​Welcome Alicia"​}}}

The context that you passed into the Absinthe.run/3 call is the same context you accessed in the third argument to the resolution function of the greeting field. After that, you’re just accessing the values you placed inside of it earlier.

Context Is Everywhere

images/aside-icons/tip.png Whatever we pass into the context is always made available as is in the resolution functions. Importantly, the context is always available in every field at every level, and it’s this property that gives it its name: it’s the “context” in which execution is happening.

Our application code, however, does not explicitly call Absinthe.run/3 but instead uses Absinthe.Plug, which executes the GraphQL documents that we receive over HTTP. We need to make sure that the context is set up ahead of time so that it has what it needs to execute documents.

Storing Auth Info in Context with a Plug

To recap the relationship between Absinthe and Plug, remember that we placed an Absinthe.Plug in our router at the API path, which looks like this:

 scope ​"​​/"​ ​do
  pipe_through ​:api
 
» forward ​"​​/api"​, Absinthe.Plug,
»schema:​ PlateSlateWeb.Schema
 
  forward ​"​​/graphiql"​, Absinthe.Plug.GraphiQL,
 schema:​ PlateSlateWeb.Schema,
 interface:​ ​:simple​,
 socket:​ PlateSlateWeb.UserSocket
 end

We’ve got a root scope that pipes requests through the :api Phoenix router pipeline, which is basically just a sequence of plugs that operate on the connection. Within this scope, we’re just passing along the conn to one of the two Absinthe.Plug plugs, which will run any query document sent to us with the specified schema.

Thankfully, Absinthe.Plug knows how to extract certain values from the connection automatically for use in the context. All we need to do is write a plug that inserts the appropriate values into the connection first.

Let’s build it! We’ll start by adding the reference to the new plug in our :api pipeline:

 pipeline ​:api​ ​do
  plug ​:accepts​, [​"​​json"​]
» plug PlateSlateWeb.Context
 end

We added the PlateSlateWeb.Context plug so that it will run prior to Absinthe.Plug and give us a place to set up our context.

 defmodule​ PlateSlateWeb.Context ​do
  @behaviour Plug
 import​ Plug.Conn
 
 def​ init(opts), ​do​: opts
 
 def​ call(conn, _) ​do
  context = build_context(conn)
  IO.inspect [​context:​ context]
  Absinthe.Plug.put_options(conn, ​context:​ context)
 end
 
 defp​ build_context(conn) ​do
 with​ [​"​​Bearer "​ <> token] <- get_req_header(conn, ​"​​authorization"​),
  {​:ok​, data} <- PlateSlateWeb.Authentication.verify(token),
  %{} = user <- get_user(data) ​do
  %{​current_user:​ user}
 else
  _ -> %{}
 end
 end
 
 defp​ get_user(%{​id:​ id, ​role:​ role}) ​do
  PlateSlate.Accounts.lookup(role, id)
 end
 end

If you’re unfamiliar with the Plug framework, that’s okay; the core idea is pretty straightforward. Let’s walk through it.

We have an init callback, which receives any options that get passed to our module (but we’re not using any of those, so we just pass them through).

The core functionality of the plug is the call/2 callback, which takes a %Plug. Conn{} struct. Inside this struct is a private key, which is a place for libraries like Absinthe to place values for later use. We use the call/2 function to return another %Plug.Conn{} struct, with our current user helpfully placed behind a context key in the private absinthe namespace. It turns out that this namespace is exactly where Absinthe.Plug looks for a prebuilt context.

We get the user in the build_context/1 function by looking up the header to get the Phoenix token sent with the request, and then using that token to find the related user (whether they’re a customer or employee). If there is no "authorization" header or if there is no user for a given API key, with will simply fall through to its else clause, where we’ll return the context without a :current_user specified.

The Accounts.lookup/2 is just a little helper function we’re going to use to help keep account-related responsibilities out of the plug itself, so it doesn’t need to worry about implementation details.

 def​ lookup(role, id) ​do
  Repo.get_by(User, ​role:​ to_string(role), ​id:​ id)
 end

With the context placed in the connection, Absinthe.Plug is properly set up to pass this value along when it runs the document, and it will be available to our resolvers. Note that we’ve got a little debugging going on with the IO.inspect [context: context]. This gives us a cheap and easy way to look at what our context is until we have something more real in place.

The final question is how to use this from within GraphiQL, because there isn’t any place in the GraphiQL interface we’ve been using to configure headers. It’s time to break out the advanced GraphiQL interface. Head over to your router and remove the interface: :simple option on the Absinthe.Plug.Graphiql plug:

 forward ​"​​/graphiql"​, Absinthe.Plug.GraphiQL,
 schema:​ PlateSlateWeb.Schema,
 socket:​ PlateSlateWeb.UserSocket

Now start your server (mix phx.server), browse to http://localhost:4000/graphiql, and behold GraphiQL Workspace as shown in the figure.

images/chp.auth/graphiql-2.png

There’s a lot more here! Let’s run the same mutation we did earlier:

This operates as it did before, but now we have the ability to configure GraphiQL to use the token that was just returned as a header to authorize future requests.

Select the token string value (without quotes) in the response box and copy it. Click the Standard drop-down, select “OAuth 2 Bearer Token,” and paste the token after Bearer.

images/chp.auth/header-1.png

After clicking OK, you should see that you’ve got a header configured in the upper right-hand part of the GraphiQL Workspace.

images/chp.auth/header-2.png

Let’s see if this header is being used to set our context correctly. Run a simple GraphQL query like this:

 {
  menuItems { name }
 }

In your console logs, you should see something like the following (we’ve cleaned it up a bit here for readability):

 [
 context:​ %{
 current_user:​ %PlateSlate.Accounts.User{
 __meta__:​ ​#Ecto.Schema.Metadata<:loaded, "users">,
 email:​ ​"​​[email protected]"​,
 id:​ 2,
 inserted_at:​ ​~​N[2017-08-29 01​:23:15​.743144],
 name:​ ​"​​Joe Hubert"​,
 password:​ ​"​​$pbkdf2-sha512$160..."​,
 role:​ ​"​​customer"​,
 updated_at:​ ​~​N[2017-08-29 01​:23:15​.744546]
  }
  }
 ]

This means it worked! The essence of an authentication system is the ability to verify that someone is who they say they are, and that’s exactly what we have here. The next step is to sort out what exactly that person is permitted to do, now that we know who they are.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.172.130