Logging out with absinthe

I am setting up authentication with Phoenix and Absinthe, using Guardian. Using Phoenix 1.3.

I followed a very helpful tutorial that got me authenticating with Absinthe. Logging is simple. I have this mutation defined in my schema:

@desc "Log in"
field :login, type: :session do
  arg :email, non_null(:string)
  arg :password, non_null(:string)
  resolve &UserResolver.login/2
end

…which points to this resolver function, which leaves me with a JWT that I store in a header:

  def login(params, _info) do
    with {:ok, user} <- Session.authenticate(params, Repo),
         {:ok, jwt, _ } <- Guardian.encode_and_sign(user, :access) do
      {:ok, %{token: jwt}}
    end
  end

Now I would like to define another schema that allows me to log out. Before using Absinthe, I did this by manipulating the conn param in my SessionsController like this:

  def delete(conn, _) do 
    {:ok, claims} = Guardian.Plug.claims(conn)
    conn
    |> Guardian.Plug.current_token
    |> Guardian.revoke!(claims)
    |> render("delete.json")
  end

However, I don’t know how to correctly access the conn variable (or the claims, or token) from within Absinthe’s framework. If I set up a logout mutation:

    @desc "Log out"
    field :logout, type: :session do
      resolve &UserResolver.logout/2
    end

…and a logout resolver function, the first parameter would refer to the args (none here) and the second to info, which seems to be a big graphql object.

This is hideously wrong, but I would like to do something like this:

def logout(_args, info) do
   conn = somehowGetAccessToConn
   {:ok, claims} = Guardian.Plug.claims(conn)
   {:ok, %{message: "Logged out"}} = Guardian.revoke!(conn, claims)
end

How does this work?

1 Like

You might add a plug, which would put conn into absinthe’s context. Like in http://absinthe-graphql.org/guides/context-and-authentication/ and How to access `conn` from different module?

2 Likes

In general I recommend against passing in the entire conn, it makes having common logic very hard. Following http://absinthe-graphql.org/guides/context-and-authentication/ you add a plug that setups up what’s called the “Context”, which is just a regular elixir map available inside of all your resolvers. There you put the token, current user, or both, and can work with each as you desire.

Do note that if you’re using JWT you’ll need to use guardian_db to actually invalidate tokens.

3 Likes

I didn’t realize Guardian worked like that. Interesting. So it seems like there are two approaches, either 1) use vanilla guardian and just delete the token from the client, or 2) use Guardian DB and actually remove the token.

If I go with #2, do you advise putting the logic inside the call function of the context? Something like this:

  def call(conn, _) do
    case isLogout(conn) do
      True ->
        # Do Guardian DB removal here
      False ->
        case Guardian.Plug.current_resource(conn) do
          nil -> conn
          user ->
            put_private(conn, :absinthe, %{context: %{current_user: user}})
        end
    end
  end

  defp isLogout(conn) do
    if Enum.at(String.split(conn.body_params["query"], " "), 3) == "logout" do True else False end
  end

What kind of client do you have? If you’ve got a JS client usually this is something the JS client handles, it just deletes the token wherever it is.

I would definitely not try to do your own parsing of the request body.

As a final minor note, logout? would be more idiomatic than isLogout.

1 Like

You may also be interested in https://github.com/absinthe-graphql/absinthe_plug/issues/109

2 Likes

Perfect. Thanks @benwilson512 !

Enum.at(String.split(conn.body_params["query"], " "), 3) == "logout"

and

if Enum.at(String.split(conn.body_params["query"], " "), 3) == "logout", do: true, else: false

return the same thing.

2 Likes