DEV Community

loading...
Cover image for How to connect Pow and Live View in your Phoenix project

How to connect Pow and Live View in your Phoenix project

Oliver Andrich
Loves coffee, code and a good conversation. // #python #django #pytorch #typescript #react #elixir #phoenix
・4 min read

Even though phx.gen.auth seems to be the hip solution for authentication at the moment in Phoenix land, I still prefer to use Pow for my small hobby projects and a side project I am working on. A short statement why I prefer Pow can be found at the end of the post.

Back in June, when I started to look a bit into Elixir and Phoenix, I blogged about adding authentication to your project using Pow. Since then, I spent part of my spare time learning more and more about Elixir and Phoenix. And I also learned that I want to use Phoenix Live View in most of my projects.

Pow and Live View need to be integrated manually

Out of the box Pow is not integrated with Live View as phx.gen.auth is. Basically, you have to implement two connections between the two libraries.

  1. On mount of a Live View you have to assign the current user to the socket object.
  2. On logout you have to tell all open live view sockets to disconnect.

#1: Assign current user to the socket

The general approach how to implement this is documented in the live view documentation

  • Create a helper function, that extracts the current user from the session and assign it to the socket.
  • Call this function in the mount function of your Live Views.

Let's start with the live view helper assign_defaults from the file lib/my_app_web/live_helpers.ex.

It picks the current user from the session and assigns it to the socket, if the user exists and is confirmed. Otherwise, it redirects to the after_sign_out_path.

All the heavy lifting is done by the get_user function. It picks the Pow session token from session, constructs a new connection struct, validates the session token and on success picks the current from the Mnesia cache. Otherwise, it returns nil.

defmodule MyAppWeb.LiveHelpers do
  @moduledoc false
  import Phoenix.LiveView

  alias MyApp.Users.User
  alias Pow.Store.CredentialsCache
  alias MyAppWeb.Pow.Routes

  def assign_defaults(socket, session) do
    socket = assign_new(socket, :current_user, fn -> get_user(socket, session) end)

    if socket.assigns.current_user do
      socket
    else
      redirect(socket, to: Routes.after_sign_out_path(%Plug.Conn{}))
    end
  end

  defp get_user(socket, session, config \\ [otp_app: :tasklist])

  defp get_user(socket, %{"my_app_auth" => signed_token}, config) do
    conn = struct!(Plug.Conn, secret_key_base: socket.endpoint.config(:secret_key_base))
    salt = Atom.to_string(Pow.Plug.Session)

    with {:ok, token} <- Pow.Plug.verify_token(conn, salt, signed_token, config),
         {user, _metadata} <-
           # Use Pow.Store.Backend.EtsCache if you haven't configured Mnesia yet.
           CredentialsCache.get([backend: Pow.Store.Backend.MnesiaCache], token) do
      user
    else
      _any -> nil
    end
  end

  defp get_user(_, _, _), do: nil
end
Enter fullscreen mode Exit fullscreen mode

The code above doesn't take into account extensions like email confirmation or blocked users. If you use something like that, you have to extend the code a bit.

The code above assumes you have activated Pows Mnesia cache module as session storage. It is strongly adviced to do this in production. But I highly recommend to do it for your development environment, too.

#2: Disconnect all live view sockets on logout

Again, the general approach is documented in the live view documentation. But the solution with Pow is not as straightforward and well documented as the first step.

It took me some time to find the correct solution in this post on the Elixir forum. And I highly recommend to stick to the ControllerCallbacks approach instead of overwriting the SessionController yourself, if the global logout is the only requirement you have. Otherwise, you would have to create a new controller and handle all cases from the activated plugins yourself.

Create a file named lib/my_app_web/pow/controller_callbacks.ex in your project and put the following code into it.

defmodule MyAppWeb.Pow.ControllerCallbacks do
  @moduledoc false
  alias Pow.Extension.Phoenix.ControllerCallbacks
  alias Plug.Conn

  @live_socket_id_key :live_socket_id

  def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, config) do
    user = conn.assigns.current_user

    conn =
      conn
      |> Conn.put_session(:current_user_id, user.id)
      |> Conn.put_session(@live_socket_id_key, "users_sockets:#{user.id}")

    ControllerCallbacks.before_respond(
      Pow.Phoenix.SessionController,
      :create,
      {:ok, conn},
      config
    )
  end

  def before_respond(Pow.Phoenix.SessionController, :delete, {:ok, conn}, config) do
    live_socket_id = Conn.get_session(conn, @live_socket_id_key)
    MyAppWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})

    ControllerCallbacks.before_respond(
      Pow.Phoenix.SessionController,
      :delete,
      {:ok, conn},
      config
    )
  end

  defdelegate before_respond(controller, action, results, config), to: ControllerCallbacks

  defdelegate before_process(controller, action, results, config), to: ControllerCallbacks
end

Enter fullscreen mode Exit fullscreen mode

After you have done that, you need to hook up your controller callbacks in the Pow configuration inside your config/config.exs file.

# Configure POW for authentication
config :my_app, :pow,
  ...
  controller_callbacks: MyAppWeb.Pow.ControllerCallbacks,
  ...
Enter fullscreen mode Exit fullscreen mode

Summary

That's it. Now you have connected Pow and Live View successfully and everything behaves like you expect it.


Why do I prefer Pow over phx.gen.auth?

  • I don't like unnecessary code generation. I only want to have the code I wrote or changed in my project.
  • i18n is much easier with Pow. You just create a module MyAppWeb.Pow.Messages in your project and can i18n the auth system in a single place. With phx.gen.auth you have to integrate gettext in round about 40 places.
  • Social login! I enjoy to support "Sign with Apple" and "Sign with Google" in my apps. So far, phx.gen.auth has no support for social login. Pow offers pow_assent, which is trivial to integrate.

But if you are looking for an authentication system for your Phoenix application you should definitely look into phx.gen.auth, too. It is a great project and I learned a lot about Phoenix from it, by digging through the source code it creates.

Discussion (10)

Collapse
karolsluszniak profile image
Karol Słuszniak

Oh just one more thing. I agree that social login is a must but I was able to integrate assent library (a part of Pow but coded as well separated lib) with phx.gen.auth pretty easily - took around 150 lines of controller + BL code to cover registration and login.

It’s probably a good idea for article too :)

Btw, you’ve mentioned Sign in with Apple, are you actually using it? AFAIK it requires having a working id of AppStore app hence my question :)

Collapse
oliverandrich profile image
Oliver Andrich Author

Yes, I am using Sign in with Apple, but I also have a registered (so not released) "real" app. This is part of an annoying detail about Sign in with Apple.

And I would be happy to see how you integrated Assent with phx.gen.auth! I am still a learner and for me it was beyond my capabilities so far.

Collapse
karolsluszniak profile image
Karol Słuszniak

Looks like someone else did it before me, just with Ueberauth instead of Assent.

iacobson.medium.com/phx-gen-auth-a...

One caveat is that Ueberauth does not support Apple auth that you’ve mentioned.

Thread Thread
oliverandrich profile image
Oliver Andrich Author

Thanks for sharing. I have to look into it and maybe it is about time for a plugin for ueberauth, that supports it.

Collapse
karolsluszniak profile image
Karol Słuszniak

Ahh thanks for the heads up, great to know that you "only" need an unreleased app :) Guess Apple doesn't care if the app is real, only if you've paid for the developer program...

As to Assent integration, I'll put writing an article about that on my todo list. Once it's there I'll gladly share.

Collapse
karolsluszniak profile image
Karol Słuszniak

Thanks for the article. I remember spending a few days putting together the whole picture of making Pow and LiveView work together. It was right before phx.gen.auth came out and I’ve decided to switch to that as an easier fit.

I don’t remember the details but I recall that Pow is designed (or at least defaults to) a very aggresive session renewal model which means that your solution may miss an important aspect - the fact that Pow session may become invalid during the LiveView lifecycle resulting in allowing a user that’s actually logged out to act like he’s authenticated.

Also, and again not sure if it’s correct so please correct me if I’m wrong, aforementioned session renewal in Pow is based on recreating session every half an hour or so and since you can’t do that from LiveView without a HTTP request you’ll end up aleays expiring session if your app routes are live.

These findings led me to abandon Pow and go for phx.gen.auth. I consider extra work like covering gettext trivial compared to overhead coming from above architectural misalignments. Also, I agree with Jose Valim’s arguments for generated solution since I’ve aleays found Devise fast at first but hard to work with later on - not worth it IMO. But of course YMMV.

Regardless, it’s great to have a choice and this post gives just that.

Collapse
oliverandrich profile image
Oliver Andrich Author

Thanks for the hints and pointers. I think, I need to investigate the session handling issue.

Collapse
karolsluszniak profile image
Karol Słuszniak

If it helps, here's a rather long discussion about these concerns: github.com/danschultzer/pow/issues.... To me it looks like there's no perfect solution - Pow seems to highly depend on regular HTTP requests and making it work with socket-only session requires either functional/security sacrifices or a lot of added complexity. But I'm no auth/Pow expert so maybe I've missed some simple way around...

Thread Thread
oliverandrich profile image
Oliver Andrich Author

Doesn't look very promising indeed. I read another thread which seemed to offer a simple solution. Seems as I need to test some code tonight. <3

Thread Thread
oliverandrich profile image
Oliver Andrich Author

@karolsluszniak I knew about this thread, but spent a lot of time yesterday and today re-reading it. And my test candidate - my wife - almost immediately ran into issues. Maybe I should switch to phx.gen.auth then and learn a bit more about assent.