Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Official documentation on how to use Pow with LiveView #663

Open
thiagomajesk opened this issue Apr 4, 2022 · 7 comments
Open

Official documentation on how to use Pow with LiveView #663

thiagomajesk opened this issue Apr 4, 2022 · 7 comments

Comments

@thiagomajesk
Copy link

thiagomajesk commented Apr 4, 2022

Hello everyone! For the lack of a better place to consolidate this information, I'm opening this issue so we can discuss something that was previously tossed around in other threads: integration with LiveView.

I'm intending to start a greenfield project that can't happen without LiveView, so I see myself again in the same spot I was a couple of years ago and it seems things haven't changed that much. I remember that I figured something out that helped me get started with it but I don't know if this is still applicable today.

Last time I hat to work with LiveView, the solution I had in place was something around these lines. I think someone posted this code in the forums, but I'm not sure
defmodule MyAppWeb.SocketCredentials do
  alias Pow.Store.CredentialsCache
  alias Pow.Store.Backend.EtsCache

  @config [otp_app: :my_app]

  def get_current_user(socket, session)

  def get_current_user(socket, %{"my_app_auth" => signed_token}) do
    with {:ok, token} <- verify_token(socket, signed_token),
         {user, _metadata} <- CredentialsCache.get([backend: EtsCache], token) do
      user
    else
      _any -> nil
    end
  end

  def get_current_user(_, _), do: nil

  defp get_connection_with_secret(socket) do
    struct!(Plug.Conn, secret_key_base: socket.endpoint.config(:secret_key_base))
  end

  defp get_salt(), do: Atom.to_string(Pow.Plug.Session)

  defp verify_token(socket, signed_token) do
    conn = get_connection_with_secret(socket)
    Pow.Plug.verify_token(conn, get_salt(), signed_token, @config)
  end
end

As you may already have noticed, we have some topics discussing this information already:

The oldest issue was created in 2019 and it seems that it's where the work is being tracked by @danschultzer. However, it seems that things have staled a little bit. LiveView is already part of Phoenix since 1.5 and Phoenix 1.6 closed the gap between Phoenix and LiveView even more.

Looking at the other issues, I can see that a lot of people got involved and many solutions have been proposed. Considering that, I can confidently say that we have a fair amount of interest to put this forward... However, a lot of information is scattered around the repo because we don't currently have official documentation on the subject. I think my main objective here is to see if we can compile the available information on this topic so we can remove part of the friction (a wiki would be better).

image

I've been using Pow for the past year on a project for a client and it has been really great. I think it's perhaps the auth framework that provided me the most joy to work with. I think that many of you must think the same and It would be a shame if we had to use something else entirely because of a "minor" thing like this. Therefore, I really think we have an urgency in documenting the solutions we have so far and what could be done to speed up the official front on supporting LiveView.

@thiagomajesk
Copy link
Author

Hi @danschultzer! Would you happen to have any updates on bringing WebSocket support to Pow for the foreseeable future?

It's been quite some time since we had any news on this. BTW, you have done amazing work with Pow thus far, however, not having a clear roadmap or ETA on the efforts to this issue, makes choosing Pow for new projects a tuff decision.

So, I think it would be useful to close the old issues which contain a lot of mixed information on the subject, and replace them with some official guidance. Perhaps compiling new findings on the alternatives or at least, providing a clear path on the types of contributions necessary to make this happen, WDYT?

@danschultzer
Copy link
Collaborator

@thiagomajesk I agree, and apologize for not having been more proactive on this. Now with Phoenix 1.7 being this liveview heavy it is more pressing than ever. I’ve been transitioning Pow to support 1.7, and will get to a second look on the sockets soon.

The pointers I got on auth socket handling are;

  • Phoenix bypasses any plug conn handling for sockets, this makes it so that using Plug.Session is required (but 99% of users will depend on Plug.Session anyhow)
  • Session cookie headers MUST be http only. JS can not read or write them. This requires session rotation happens on a regular http request:
    • A http ping being send every X seconds
    • A regular HTTP request triggered when navigating every once in a while
  • Socket connection only have to be authenticated when connecting the first time and reconnecting after interruption. Only application authorization is necessary while socket is active. The process needs to listen to session invalidation to force close the socket connection.

For Pow this means a few changes:

  • Surface session invalidation from TTL (this goes in hand with my plan on full observability for session lifecycle)
  • Native support for WebSocket that can be used for LiveView as well (though live view already has live_session macro)

FWIW there’s no OWASP guidelines here, and Pow being very industry standards heavy makes it more tricky to decide what is the most appropriate solution.

I’ve been guiding a larger organization to use Elixir. Time has been extremely limited due to this and personal life. The good news is that I’m finally getting to a position where I can dogfood Pow again. That means I can put a lot more time into this.

It’ll still be some weeks before I get to this, so I will appreciate any help with docs/guides or PR’s to Pow.

@brecke
Copy link

brecke commented Jun 15, 2023

Just wanted to pop in and thank @thiagomajesk for that code snippet above. I'm on a greenfield 1.7 project trying to use both traditional and live views and it just saved my day. I think I have it all working for a basic authentication mechanism thanks to that.

For reference, here are the important snippets:

  alias APPWeb.SocketCredentials

  def mount(_params, session, socket) do
    current_user = SocketCredentials.get_current_user(socket, session)
    socket = assign(current_user: current_user)
    {:ok, socket}
  end

and don't forget to adapt def get_current_user(socket, %{"COOKIE_NAME_AUTH" => signed_token}) do in socket_credentials.ex.

Is there anything you might have found that needs patching @thiagomajesk? Considering this workaround, I'm not sure why anyone would think pow is not suitable for new projects. Am I missing something?

@thiagomajesk
Copy link
Author

thiagomajesk commented Jun 15, 2023

Hi @brecke I'm glad that helped... The main "problem" with Pow and LV so far I think, is the lifespan of the session. Pow has a very aggressive session expiration policy and since a WebSocket is a stateful connection, we don't have a way to guarantee those sessions are in sync.

In practice, this means that once a user is authenticated in a LiveView, he can stay there forever even if his session is no longer valid for other parts of the app (HTTP). This might not be desirable for a number of reasons and for authorization, it might be an even bigger problem.

Other concerns like security and the session expiration were also pointed out in the initial issue: #271 by @danschultzer.

@brecke
Copy link

brecke commented Jun 15, 2023

That makes total sense. I'll keep that in mind, thank you.

@brecke
Copy link

brecke commented Jun 21, 2023

Hi @thiagomajesk I've been thinking about this and doing some experiments about what you said and I think there's a hack that may be helpful. I'm very new to elixir so bear with me.

If I understand correctly the main scenario here is how to deal with the LV connection when the cookie is gone, right? That could be via either signing out or expiration. So I did this test: two windows side by side, one on / http-served homepage and another on /profile live view, with a handle_info handler for the : tick event:

on_mount({SocketCredentials, :default})

  def mount(_params, session, socket) do
    # Just debugging
    if connected?(socket) do
      :timer.send_interval(1000, self(), :tick)
    end

    socket =
      assign(socket,
        seconds: 0,
        session: session
      )

    {:ok, socket}
  end

...

  def handle_info(:tick, socket) do
    current_session = SocketCredentials.get_current_user(socket, socket.assigns.session)
    if is_nil(current_session) do
      {:halt, redirect(socket, to: "/login")}
    else
      {:noreply, assign(socket, seconds: socket.assigns.seconds + 1)}
    end
  end

In a nutshell, two things are happening here:

  1. live view page does not mount if not logged in
  2. live view page checks for valid cookie session every second, and exits if not logged in

Having two browser windows side by side seems to work this way: if you sign out on the http one, the live one exits after ~1s. Now I'll assume this ain't pretty, and that sending a :tick every second might be asking for trouble, but this kind of seems one hacky way to solve this.

A better one would be to have live views subscribe to some no_session event via Phoenix.PubSub but I'm not sure how that would go.

@thiagomajesk
Copy link
Author

thiagomajesk commented Jun 21, 2023

I personally wouldn't do something like this in a production application, not just because of performance, but security as well. Effectively, you will always have a "renew window" of 1s that can be exploited. BTW, if you are only reading the session cookie, I guess you also lose the ability to implement sliding expiration for LiveViews, unless you explicitly renew the session manually every time on each LV.

A better one would be to have live views subscribe to some no_session event via Phoenix.PubSub but I'm not sure how that would go.

Yes, ideally Pow should allow us to know beforehand when a user session expires so we can broadcast a disconnect to all LiveViews, but I'm not sure about the specifics of how this should be done internally. I think it would probably require a process to monitor users' sessions from time to time, but I'm surely overly simplifying the problem 😅.

The approach we are talking about is documented here: https://hexdocs.pm/phoenix_live_view/0.19.3/security-model.html#disconnecting-all-instances-of-a-live-user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants