Why doesn't Absinthe expose Plug.Conn?

Absinthe seems to work with an Absinthe.Resolution instead of Plug.Conn, however this makes interacting with Plug.Conn quite difficult, the only way seeming to be to hook into the before_send callback of Absinthe.Plug.

In cases where you need to change the session or cookies based on the return value of a resolver (for example a login mutation), then it’s not immediately obvious how to go about doing that. My current approach is to use a piece of middleware to put the resolution value on the resolution context, and then in the before_send callback grab that information and do whatever I need to do with Plug.Conn.

So my questions are:

  1. Why doesn’t Absinthe expose Plug.Conn for use in mutations?
  2. Is my current method of interacting with Plug.Conn (using the resolution context and before_send callback) the recommended best practice?

I think most people (including me, and others from what I’ve gathered here and in the slack channel) handle login outside of graphql, that way all the graphql requests are fully authenticated already. Then it’s just a matter of adding what authentication info you need into the absinthe resolution context.

3 Likes

Thanks for the quick response, I had thought that might be what people were leaning towards… It just seems odd that Absinthe doesn’t provide you with a nice way of handling this situation.

GraphQL, in theory, do not require HTTP. Sometimes it is via WebSockets, but it can be over any other protocol you want. Nothing in standard prevents you from serving GraphQL queries over SCTP with ETF as a transport format.

3 Likes

Ahhh that’s interesting, I hadn’t realised this.

In that case could we not (and I’m totally spitballing here so I may be way off base) provide some kind of generic way to feed in a Plug.Conn or it’s equivalent for another transport protocol? Maybe a middleware macro where Absinthe can pass in whatever struct represents the protocol currently being used (e.g. Plug.Conn) and you can pattern match the arguments provided in your middleware module callback to do Plug.Conn specific work?

For example:

    field :login, type: :user_result do
      arg(:email, non_null(:string))
      arg(:password, non_null(:string))

      resolve(&Accounts.login/3)
      after_resolve(ScribeWeb.Schema.Middleware.SessionCreator)
    end

where SessionCreator is a middleware module with a callback accepting Plug.Conn or it’s equivalent.

This still isn’t great though because it means you have to split up work between the resolver and the middleware. This is annoying when the middleware can have multiple outcomes depending on what happens in the resolver. For example in Accounts.login I have two successful outcomes:

  1. The user is already logged in so they don’t need a new session/refresh token
  2. The user isn’t logged in but the details provided are correct so new a session/refresh token pair are created

In this case I would need some way of telling the middleware whether or not I would like the session and refresh token to be created and placed on Plug.Conn, which isn’t obvious. One way would be to put an action on the resolution context (as a keyword list where the value indicates the type of action, e.g. :update_session) that the middleware would be listening for. But again, this feels a little off.

So maybe instead there could be a field on Absinthe.Resolution (e.g. after_resolve) that accepted a tuple containing a keyword mapping to a transport protocol (e.g. :http, :websocket) and a function (or multiple functions) that will be called after the field has been resolved. If the protocol specified was being used then these functions would be called with the relevant structs being passed in.

I know all of these potential solutions force you to think about the transport protocol in the schema, where ideally the schema should be protocol agnostic. However I don’t really see what benefit not allowing you to do this brings, considering that this seems to be a somewhat common use case. Absinthe already has a before_send callback that passes in Plug.Conn to try and tackle this, but I just think it’s a bit clunky as the work is being done very far from the code that is causing the work to happen. Again could be way off base so I’m interested to hear what you think.

Absinthe.Plug has this, Absinthe does not. Absinthe will continue to insist that GraphQL operations are handled without regard for transport. Absinthe.Plug is welcome to map Plug.Conn data to the context and map context info back to the conn, and I’m happy to consider proposals that make that more streamlined.

If you mean Absinthe middleware here, consider just ditching a resolver for that operation and just using middleware. resolve is a thin layer around middleware anyway.

2 Likes

Rigggghhht, so is Absinthe.Plug’s purpose just too map Plug.Conn data to context and back again?

As a possible solution, could you have an extra field (in addition to context) on the resolution that allows you to give it functions. These functions are then called by Absinthe.Plug automatically if they exist, and Absinthe.Plug passes in Plug.Conn which you can interact with. For example (quite rough):

      def update_conn(conn) do
          whatever_needs_doing
      end
      Map.update!(resolution, :after_resolve, fn actions ->
        actions ++ [update_conn]
      end)

I’m still quite new to Elixir so I haven’t made any use of macros but maybe the Map.update side of things (to update the resolution struct) could also be captured in a macro to make it a little less verbose.

Hi there :wave:

I have been trying to find more information about what you’re describing here.

Basically I would do a post to a login endpoint, get authenticated and then set a cookie, and then be redirected to a page that uses a client (Apollo maybe) to make GraphQL requests. “Then it’s just a matter of adding what authentication info you need into the absinthe resolution context”. (can you elaborate?)

This sounds like what I want but I’d like to see some examples. All blog posts/videos I have come across are using Guardian, or JWT. I’m looking for examples with hand rolled auth using a cookie. Can you elaborate on the two parts of this?

Part 1. Logging in without GraphQL, setting the session, and redirecting to an SPA/Client
Part 2. Ensuring all subsequent http requests are authenticated and authorized in the “resolution context”

Thank you for any guidance or direction (links to articles or github repos perhaps?) you can provide
:pray: :pray: :pray:

Have you looked at the context and authentication guide in the docs? https://hexdocs.pm/absinthe/context-and-authentication.html#context-and-plugs
You create a plug and call Absinthe.Plug.put_options(conn, context: context) where context has something like: {current_user: user, login_count: 9} and whatever other info you need in your resolvers for your context.

I’d recommend looking into Pow: Robust, modular, extendable user authentication and management system for the authentication if you don’t currently have anything, but plain hand-rolled cookies can be fine too, although I don’t have any guides handy for that.

Edit: sent early

2 Likes