Open goulvenclech opened 1 year ago
Found this gist -> https://dev.to/oliverandrich/how-to-connect-pow-and-live-view-in-your-phoenix-project-1ga1
Really helpful, and answered my questions. But I still think we could have a guide about it in the doc or somewhere.
The link is
Found this gist -> https://dev.to/oliverandrich/how-to-connect-pow-and-live-view-in-your-phoenix-project-1ga1
Really helpful, and answered my questions. But I still think we could have a guide about it in the doc or somewhere.
This link gives a 404 now.
This is how I deal with it at the moment:
# TODO: Remove when upstream Pow can handle LiveView/socket auth
defmodule MyAppWeb.Pow.Phoenix.LiveView do
@moduledoc false
use MyAppWeb, :verified_routes
def on_mount(:require_authenticated, _params, session, socket) do
socket = mount_current_user(socket, session)
if socket.assigns.current_user do
{:cont, socket}
else
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You must be logged in to access this page.")
|> Phoenix.LiveView.redirect(to: ~p"/")
{:halt, socket}
end
end
defp mount_current_user(socket, session) do
Phoenix.Component.assign_new(socket, :current_user, fn ->
pow_config = [otp_app: :my_app_web]
{_conn, user} =
%Plug.Conn{
private: %{
plug_session_fetch: :done,
plug_session: session,
pow_config: pow_config
},
owner: self(),
remote_ip: {0, 0, 0, 0}
}
|> Map.put(:secret_key_base, MyAppWeb.Endpoint.config(:secret_key_base))
|> Pow.Plug.Session.fetch(pow_config)
user
end)
end
end
defmodule MyAppWeb.Router do
# ...
scop "/" do
live_session :user_protected, on_mount: [
{MyAppWeb.Pow.Phoenix.LiveView, :require_authenticated}
] do
# ...
end
end
end
This is my temporary fix until I get Pow to work out-of-the-box with LiveView. Why it hasn't been done already is due to the million other things I have to tend to. There's a bunch of blockers and caveats to deal with with websockets in Phoenix:
I'm planning to solve three first, to make it safe to use Pow with websockets. This requires a refactor of how session stores currently work.
To answer directly the questions for anyone else following:
- To retrieve the current_user with POW, should I use
Pow.Plug.current_user()
orPow.Store.CredentialsCache.get()
? What are the differences, and what is the modern method?
Pow.Store.CredentialsCache.get/2
is too low-level. Pow.Plug.Current_user/1
could be used, but it expects the plug pipeline to have been run, so it's kinda implicit. I find it clearest to call Pow.Plug.Session.fetch/2
, with an expectation that the Pow.Plug.Session
has been called.
- From what I understand, as these two functions require a
conn
, I have to create a plug with aput_session()
function to add the current user ID -> Is there a recommended way to do this? Where should the plug be?
This is handled on the first load when the request goes through the plug pipeline. Plug.Session
is used by default so you will have access to the session there. You won't have to deal with setting the session at all. How I wish we could just have the whole initial conn instead, would make it much easier to make Pow work with websockets.
- Once the current user ID is in the session, do I have to
assign_new(socket, :current_user, Users.get_user(session["current_user_id"])
in themount()
of each LiveView? Or there's a more practical way to do it?
I recommend using the live_session/2
as shown above. It's probably the cleanest approach.
A guide would be good, but this is really something Pow should provide out of the box now that Phoenix LiveView is included in Phoenix 1.7. The guide would need to cover some of the caveats above regarding security (session rotation, etc). If anyone wants to write one let me know, it could go up on https://powauth.com/guides/ along with general recommendations for websocket security.
Hi @danschultzer ,
Thanks for your answer. I created a util UserLiveAuth defining an on_mount function to retrieve and then assign the user based on your comment (see code below). And everything works perfectly locally.
Sadly, this does not prove to be reliable in production (app and DB hosted on Fly): sometimes everything works, sometimes the function returns nil every other time, and often the user is disconnected for no reason. Here is an example in the monitoring logs, where I constantly update the page, with inconsistent responses:
[info] 18:46:37.986 request_id=F4oQLanygBo_-oMAAAbh [info] Sent 200 in 17ms
[info] 18:46:38.335 request_id=F4oQLb_EKOAJCSUAAAcB [info] GET /
[info] 18:46:38.342 request_id=F4oQLb_EKOAJCSUAAAcB [info] Elixir.MyappWeb.UserLiveAuth: No user found in session
[info] 18:46:38.352 request_id=F4oQLb_EKOAJCSUAAAcB [info] Sent 200 in 17ms
[info] 18:46:38.653 request_id=F4oQLdK1RR-SfJgAAAch [info] GET /
[info] 18:46:38.660 request_id=F4oQLdK1RR-SfJgAAAch [info] Elixir.MyappWeb.UserLiveAuth: User goulvenclech found in session
[info] 18:46:38.669 request_id=F4oQLdK1RR-SfJgAAAch [info] Sent 200 in 16ms
[info] 18:46:38.967 request_id=F4oQLeVzuYH-qAAAAAdB [info] GET /
[info] 18:46:38.975 request_id=F4oQLeVzuYH-qAAAAAdB [info] Elixir.MyappWeb.UserLiveAuth: No user found in session
I tried fetching the current_user in two different ways (see code below), with CredentialsCache.get()
and Session.fetch()
, giving a similar result.
Do you think this is an error in the architecture of my code? In my way of using POW? Or could this be related to my hosting (Fly.Io)?
Thanks for any help, and your work on this library.
Utils module :
defmodule MyappWeb.UserLiveAuth do
import Phoenix.Component
alias Surface.Components.Context
def on_mount(:default, _params, session, socket) do
user = get_user(socket, session)
{:cont,
socket
|> assign_new(:current_user, fn -> user end)
|> Context.put(current_user: user)}
end
end
First version of get_user :
defp get_user(socket, session, config \\ [otp_app: :myapp])
defp get_user(socket, %{"myapp_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)
Logger.info("#{__MODULE__}: get_user/3 called with #{signed_token}")
with {:ok, token} <- Pow.Plug.verify_token(conn, salt, signed_token, config),
{user, _metadata} <- CredentialsCache.get([backend: EtsCache], token) do
Logger.info("#{__MODULE__}: get_user/3 found user #{user.username}")
user
else
resp ->
Logger.error("#{__MODULE__}: get_user/3 failed with error #{resp}")
nil
end
end
defp get_user(_, _, _), do: nil
Second version of get_user , based on your comment :
defp get_user(socket, session, config \\ [otp_app: :myapp]) do
case %Plug.Conn{
private: %{
plug_session_fetch: :done,
plug_session: session,
pow_config: config
},
secret_key_base: socket.endpoint.config(:secret_key_base),
owner: self(),
remote_ip: {0, 0, 0, 0}
}
|> Pow.Plug.Session.fetch(config) do
{_conn, nil} ->
Logger.error("#{__MODULE__}: No user found in session")
nil
{_conn, user} ->
Logger.info("#{__MODULE__}: User #{user.username} found in session")
user
end
end
@goulvenclech Looks like you are using the EtsCache. It will reset each time you restart/redeploy (or be fragmented if you are running in a cluster). You should instead use a persistent cache, either Mnesia, Redis, or Postgres as described here: https://github.com/pow-auth/pow#cache-store
@danschultzer Hum I'm not sure that's the problem... Do you think I didn't configure Mnesia well ?
In mix.exs
:
def application do
[
mod: {Myapp.Application, []},
extra_applications: [:logger, :runtime_tools, :mnesia]
]
end
In config.exs
:
config :myapp, :pow,
web_module: MyappWeb,
user: Myapp.Users.User,
repo: Myapp.Repo,
cache_store_backend: Pow.Store.Backend.MnesiaCache,
extensions: [PowResetPassword, PowEmailConfirmation, PowPersistentSession]
in application.ex
:
def start(_type, _args) do
children = [
[...]
# Start POW (authentication) cache
Pow.Store.Backend.MnesiaCache
]
Did I forgot something?
Oh sorry, I was reading the first version you posted. Yeah it all looks correct. What setup do you have on fly.io? Are you running more than one node at any time? And how do you deal with deployment? That it returns nil every other time, sounds like competing nodes.
Hi everyone,
What is the standard way to retrieve "current_user" in Liveview using POW?
I'm starting a project from scratch with LiveView and POW, and despite several topics discussing the problem, I can't find any documented solution or tutorial. Even less on recent versions.
My questions:
Pow.Plug.current_user()
orPow.Store.CredentialsCache.get()
? What are the differences, and what is the modern method?conn
, I have to create a plug with aput_session()
function to add the current user ID -> Is there a recommended way to do this? Where should the plug be?assign_new(socket, :current_user, Users.get_user(session["current_user_id"])
in themount()
of each LiveView? Or there's a more practical way to do it?A guide on the subject, particularly in the docs, would be welcome.
Thanks for any help :)