Private Absinth GraphQL subscriptions/channels per user?

Hello all,

I am trying to build my first GraphQL subscription using Absinthe. I got as far as making the subscription itself using the authenticated user socket (with Guardian), but I am a bit lost on how to make a “per-user” channel that only a logged in user subscribes to.

The subscription itself is simple and it’s purpose it’s getting a download link once it’s ready from a background job:

# in schema.ex
subscription do
    field :download_ready, :download_link do
      config(fn _args, %{context: ctx} ->
        {:ok, topic: "user_download"}
      end)
    end
end

If I leave topic as “user_download” I have no problem in subscribing, I can of course pass the user ID as argument and append it to the topic: user_download:#{args[:id]}.

However, I think passing it as argument is wrong, since an authenticated socket is used and already carry this information. The client subscription should ideally stay as simple as:

subscription {
  downloadReady {
    url
  }
}

So that particular user is automatically subscribed and getting only his/her notifications.

Unfortunately I don’t have access to the signed socket from the first mentioned field :download_ready block. The context there is not a socket context.

Maybe I am thinking about this wrong. How do you tackle the need of private per-user subscriptions/channels?

Hey @strzibny, in your particular case could you do:

      config(fn _args, %{context: ctx} ->
        {:ok, topic: "user:#{ctx.current_user.id}"}
      end)

Then the actual topic of the subscription downloadReady will be per user, without having to pass it in as an argument.

3 Likes

The trouble I am having is that the context won’t have the current_user or anything I set on the signed socket. I tried to explain it, but perhaps failed to be more clear. The way you suggest is exactly the way I tried and expected it to work.

Can you show what you have in your socket connect function?

Yes, it’s pretty much standard (I commented out passing current login as struct, but neither getting id or passing this works for me):

defmodule MyWeb.LoginSocket do
  use Phoenix.Socket

  use Absinthe.Phoenix.Socket,
    schema: MyWeb.Private.Schema

  def connect(%{"Authorization" => header_content}, socket) do
    [[_, token]] = Regex.scan(~r/^Bearer (.*)/, header_content)

    case Guardian.Phoenix.Socket.authenticate(socket, MyWeb.Public.Guardian, token) do
      {:ok, authed_socket} ->
        # socket =
        #   Absinthe.Phoenix.Socket.put_options(socket,
        #     context: %{
        #       current_login: "test"
        #     }
        #   )

        {:ok, authed_socket}

      {:error, _} ->
        :error
    end
  end

  # This function will be called when there was no authentication information
  def connect(_params, socket) do
    :error
  end

  def id(_socket), do: nil
end

Note that authentication happens and I get correct credentials in authed_socket.assigns.guardian_default_claims["sub"]. It’s just not what I get on the other side in Absinth GraphQL definitions.

1 Like

So put_opts has to be used.

def connect(%{"Authorization" => header_content}, socket) do
    [[_, token]] = Regex.scan(~r/^Bearer (.*)/, header_content)

    case Guardian.Phoenix.Socket.authenticate(socket, DigiWeb.Public.Guardian, token) do
      {:ok, authed_socket} ->
        login_id = authed_socket.assigns.guardian_default_claims["sub"]

        new_socket =
          Absinthe.Phoenix.Socket.put_opts(authed_socket,
            context: %{
              current_login: login_id
            }
          )

        {:ok, new_socket}

      {:error, _} ->
        :error
    end
  end

You then need to access it as context[:context][:current_login]

2 Likes

I also tried to write a bit more about this here http://nts.strzibny.name/graphql-subscriptions-with-elixir-and-absinth/ if anybody come across this.

1 Like