3

I use the built-in GraphiQL interface in Absinthe. As follows:

  pipeline :browser do
    plug RemoteIp, headers: ~w[x-forwarded-for], proxies: ~w[]
    plug :accepts, ["html", "json"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  scope "/graphiql" do
    pipe_through :browser # Use the default browser stack

    forward "/", Absinthe.Plug.GraphiQL,
            schema: ApiWeb.Schema,
            default_headers: {__MODULE__, :graphiql_headers},
            context: %{pubsub: ApiWeb.Endpoint}
  end

  def graphiql_headers(conn) do
    %{
      "X-CSRF-Token" => Plug.CSRFProtection.get_csrf_token(),
    }
  end

I need the end-user to insert an Authentication: Bearer <JWT> in the interface, and then need to unwrap it for the sub: header, which contains my user id, which I need to use in the resolvers.

The user can configure custom headers, that's no problem. If he then executes a GraphSQL query, the interface will issue a POST to the /graphiql endpoint. It is at this point I want to invoke a few plugins which check the JWT and retrieve user info.

I thought I could use the default_headers option, but that seems only to be invoked during GET requests.

It seems I need different pipelines for GET and POST to the /graphiql endpoint, how do I accomplish that? I must be doing something wrong...

Note that if I use the same pipeline for GET and POST, the JWT will already be checked on just visiting the endpoint in the browser, which I don't want.

raarts
  • 2,711
  • 4
  • 25
  • 45

1 Answers1

2

Yes, actually I did the following:

  pipeline :authenticate_on_post_only do
    plug ApiWeb.Plugs.Authenticate, post_only: true
  end

  scope "/graphiql" do
    pipe_through [:browser, :authenticate_on_post_only]

    forward "/", Absinthe.Plug.GraphiQL,
            schema: ApiWeb.GraphQL,
            socket: ApiWeb.GraphQLSocket
  end

combined with:

defmodule ApiWeb.Plugs.Authenticate do
  use Plug.Builder
  alias ApiWeb.Helpers.JWT

  plug Joken.Plug, verify: &JWT.verify/0, on_error: &JWT.error/2
  plug ApiWeb.Plugs.Subject
  plug Backend.Plug.Session

  def call(%Plug.Conn{method: "POST"} = conn, opts) do
    conn = super(conn, opts) # calls the above plugs
    put_private(conn, :absinthe, %{context: conn})  # for absinthe (GraphQL), for resolvers to re-use
  end
  def call(conn, opts) do
    if opts[:post_only] do
      conn
    else
      super(conn, opts) # calls the above plugs
    end
  end
end

Of course you can use any of your own authentication plugs, instead of the ones I listed.

I also had a REST API in the same module, which I used as follows:

  scope "/v1", ApiWeb do
    pipe_through :api

    <my resources here>
  done

with the api pipeline defined as:

  pipeline :api do
    plug :put_resp_content_type, "application/json"
    plug :accepts, ["json"]
    plug ApiWeb.Plugs.Authenticate
  end

which would authenticate on any type of HTTP request.

raarts
  • 2,711
  • 4
  • 25
  • 45
  • I know this is a few years too late, but I greatly appreciate you adding your own solution to your own question - it helped me to solve my own issue with authenticating a absinthe application :) – tommica Sep 12 '21 at 18:56
  • Exactly why I did it. Happy it was useful. – raarts Sep 12 '21 at 19:43