Phx.gen.auth calls mount first despite route being in Authenticated routes

Phoenix 1.6.6
Liveview 0.17.6

I’m running into a weird problem after I upgraded to Phoenix 1.6. I have a LiveView route that is behind the Authentication wall. But the LiveView mount3 is being called BEFORE the authentication is invoked. This is raising an error because I use the mount to assign my user to the socket. Code below.

QUESTION: Why is mount being called before the user is forced to login? Shouldn’t phx.gen.auth force the user to first log in if the route is sitting behind a pipe_through [:browser, ::require_authenticated_user]?

CODE:
User clicks button which invokes a handle_event that redirects user:

def handle_event("manage_resources", _params, socket) do
    {:noreply, push_redirect(socket, to: Routes.resourceform_manage_resources_path(socket, :index), replace: true)}
 end

Route is sitting behind the authentication wall:

scope "/", MyAppWeb do
    pipe_through [:browser, :require_authenticated_user]
…
live "/manage-resources", ResourceformLive.ManageResources, :index
…
end

Run Time error raised:

RESOURCEFORM.modify_resources.mount (**NOTE: this is log output telling me that mount has been called**)

[error] GenServer #PID<0.11481.0> terminating
** (UndefinedFunctionError) function nil.id/0 is undefined
    nil.id()
    (my_app 0.1.0) lib/my_app_web/live/resourceform_live/manage_resources.ex:23: MyAppWeb.ResourceformLive.ManageResources.mount/3
(phoenix_live_view 0.17.6) lib/phoenix_live_view/utils.ex:301: anonymous fn/6 in Phoenix.LiveView.Utils.maybe_call_live_view_mount!/5
    (telemetry 0.4.2) /Users/annad/files/Projects/MyAppLinks/App/my_app_links/deps/telemetry/src/telemetry.erl:262: :telemetry.span/3
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/channel.ex:949: Phoenix.LiveView.Channel.verified_mount/8
    (phoenix_live_view 0.17.6) lib/phoenix_live_view/channel.ex:58: Phoenix.LiveView.Channel.handle_info/2
    (stdlib 3.17) gen_server.erl:695: :gen_server.try_dispatch/4
    (stdlib 3.17) gen_server.erl:771: :gen_server.handle_msg/6
    (stdlib 3.17) proc_lib.erl:226: :proc_lib.init_p_do_apply/3

[info] GET /manage-resources
[debug] Processing with Phoenix.LiveView.Plug.index/2
  Parameters: %{}
  Pipelines: [:browser, :require_authenticated_user]
[info] Sent 302 in 1ms
[debug] Phoenix.Router halted in :require_authenticated_user/2
[info] GET /users/log_in
[debug] Processing with MyAppWeb.UserSessionController.new/2
  Parameters: %{}
  Pipelines: [:browser, :redirect_if_user_is_authenticated]

As you can see above, mount is called and then the router pipelines it to [:browser, :require_authenticated_user]. The problem is that I use mount3 to set the user and grab it’s ID. That is why I’m getting that run time error in the first mount:

@impl true
  def mount(_params, session, socket) do
    IO.puts "RESOURCEFORM.modify_resources.mount"

    # Assign user to socket if not already assigned
    socket = assign_current_user(socket, session)

    # Get User (authenticated user)
    user = socket.assigns.current_user

    socket =
      socket
        |> assign(:user_creator_id, user.id )
        |> assign(:resource_forms, list_managed_resourceforms(user.id))
        |> assign(:return_to,
              Routes.resourceform_manage_resources_path(socket, :index))

    {:ok, socket}
  end

How do I force login BEFORE the mount?

This post is a little old and I don’t know if you’re still looking for help with this, but someone else may read it and find it useful. I think this is a misunderstanding of push_redirect/2. From the docs:

The current LiveView will be shutdown and a new one will be mounted in its place, without reloading the whole page.

push_redirect/2 reuses the same websocket and does not make a new HTTP request, so you are not using your auth plug before mounting the new live view. Only when your live view crashes because it doesn’t know how to handle a nil value for current_user does the page actually reload and use your plug.

Use redirect/2 instead which will reload the page with a new HTTP request, forcing a call to your auth plug in the process. From the docs:

Annotates the socket for redirect to a destination path.

Note: LiveView redirects rely on instructing client to perform a window.location update on the provided redirect location. The whole page will be reloaded and all state will be discarded.

Once you’ve established your websocket connection behind the authentication wall you can use push_redirect/2 to avoid new HTTP requests, however you should also be performing your user auth on every new live view mount. You can do this by creating an on_mount/4 hook which you can use very similarly to your plug. There are examples in the docs.

LiveView begins its life-cycle as a regular HTTP request. Then a stateful connection is established. Both the HTTP request and the stateful connection receive the client data via parameters and session.

This means that any session validation must happen both in the HTTP request (plug pipeline) and the stateful connection (LiveView mount).

1 Like

First … thank you for taking the time to reply! I am really grateful for all of the help I’ve gotten from this community, so much appreciated!

Yep … I ended up with an on_mount function that checks for nil. I put the implemenation in my live_helpers.ex file. For anyone who runs into this problem, here is the code along with the comments that I included in my file:

@doc """
  on_mount added to allow liveview to check authentication at
  mount and redirect user to login if they are not logged in.

  This is to be called only by LiveViews that require authentication.
  Added Feb2022 with new LiveView V0.17
  https://hexdocs.pm/phoenix_live_view/security-model.html#mounting-considerations
  """
  def on_mount(:default, _params, session, socket) do

    # Assign user to socket if not already assigned
    socket = assign_current_user(socket, session)

    if socket.assigns.current_user == nil do
      {:halt, redirect(socket, to: "/users/log_in")}
    else
      {:cont, socket}
    end
  end

 
  def assign_current_user(socket, session) do
    assign_new(
      socket,
      :current_user,
      fn -> Users.get_user_by_session_token(session["user_token"])
    end )
  end
2 Likes