I'm working in a project that is mostly a freshly generated web app from phx.new
and phx.gen.auth
. I have an index page that is non-liveview. After login, the user is redirected to the main page, which is a liveview.
The expectation: After clicking the generated Log out
link, the user should be redirected to the /
index page, which is not a liveview. This behavior is specified by the generated authentication.
The experience: The problem is that when I click the generated Log out
link, instead of being redirected to the logged out index splash page, as the generated authentication is written to do, instead I'm redirected to the login page, where I see two flash messages: one :info
flash indicating successful logout, and a second :error
flash complaining ""You must be logged in to access this page." I don't want users to see that :error
flash on the login page, and worse, I think, is the fact that the reason that :error
flash is appearing is because the PageLive
liveview, which is not present on the index page, is running its mount/3
function again (a third time), which is causing the liveview authentication to run again, and cause a second redirect. Importantly, this issue occurs intermittently, i.e. sometimes the redirect works correctly and sends the user to the index page without issue, and other times the second, redundant redirect and the mistaken flash message is displayed. I think this indicates some kind of race condition.
I have a relatively newly generated project with these routes (among others):
router.ex
scope "/", MyappWeb do
pipe_through :browser
live_session :default do
live "/dash", PageLive, :index
end
end
scope "/", MyappWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/", PageController, :index
end
scope "/", MyappWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
end
The authentication was generated by phx.gen.auth
. The delete
action in the generated UserSessionController
triggers the generated UserAuth.log_out_user/1
to fire.
user_session_controller.ex
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> UserAuth.log_out_user()
end
user_auth.ex
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_session_token(user_token)
if live_socket_id = get_session(conn, :live_socket_id) do
MyappWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
end
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: "/")
end
The live route in the router to /dash
routes through a liveview called PageLive
, which simply mounts over some authentication, as recommended in liveview docs:
page_live.ex
defmodule MyappWeb.PageLive do
use MyappWeb, :live_view
alias MyappWeb.Live.Components.PackageSearch
alias MyappWeb.Live.Components.Tabs
alias MyappWeb.Live.Components.Tabs.TabItem
on_mount MyappWeb.UserLiveAuth
end
user_live_auth.ex
defmodule MyappWeb.UserLiveAuth do
import Phoenix.LiveView, only: [assign_new: 3, redirect: 2]
alias Myapp.Accounts
alias Myapp.Accounts.User
alias MyappWeb.Router.Helpers, as: Routes
def mount(_params, session, socket) do
socket =
assign_new(socket, :current_user, fn ->
find_current_user(session)
end)
case socket.assigns.current_user do
%User{} ->
{:cont, socket}
_ ->
socket =
socket
|> put_flash(:error, "You must be logged in to access this page.")
|> redirect(to: Routes.user_session_path(socket, :new))
{:halt, socket}
end
end
defp find_current_user(session) do
with user_token when not is_nil(user_token) <- session["user_token"],
%User{} = user <- Accounts.get_user_by_session_token(user_token),
do: user
end
end
Here's the log of the process after the user clicks log out:
**[info] POST /users/log_out**
[debug] Processing with MyappWeb.UserSessionController.delete/2
Parameters: %{"_csrf_token" => "ET8xMSU5KSEedycKEAcJfX0JCl45LmcF_VEHANhinNqHcaz6MFRkIqWu", "_method" => "delete"}
Pipelines: [:browser]
[debug] QUERY OK source="users_tokens" db=1.8ms idle=389.7ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."first_name", u1."last_name", u1."username", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<159, 144, 113, 83, 223, 12, 183, 119, 50, 248, 83, 234, 128, 237, 129, 112, 138, 147, 148, 100, 67, 163, 50, 244, 127, 26, 254, 184, 102, 74, 11, 52>>, "session", ~U[2021-10-06 22:13:44.080128Z]]
[debug] QUERY OK source="users_tokens" db=1.7ms idle=391.8ms
DELETE FROM "users_tokens" AS u0 WHERE ((u0."token" = $1) AND (u0."context" = $2)) [<<159, 144, 113, 83, 223, 12, 183, 119, 50, 248, 83, 234, 128, 237, 129, 112, 138, 147, 148, 100, 67, 163, 50, 244, 127, 26, 254, 184, 102, 74, 11, 52>>, "session"]
**[info] Sent 302 in 6ms**
**[info] CONNECTED TO Phoenix.LiveView.Socket in 64µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"_csrf_token" =>** "ET8xMSU5KSEedycKEAcJfX0JCl45LmcF_VEHANhinNqHcaz6MFRkIqWu", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"}
[debug] QUERY OK source="users_tokens" db=1.6ms idle=422.5ms
SELECT u1."id", u1."email", u1."hashed_password", u1."confirmed_at", u1."first_name", u1."last_name", u1."username", u1."inserted_at", u1."updated_at" FROM "users_tokens" AS u0 INNER JOIN "users" AS u1 ON u1."id" = u0."user_id" WHERE ((u0."token" = $1) AND (u0."context" = $2)) AND (u0."inserted_at" > $3::timestamp + (-(60)::numeric * interval '1 day')) [<<159, 144, 113, 83, 223, 12, 183, 119, 50, 248, 83, 234, 128, 237, 129, 112, 138, 147, 148, 100, 67, 163, 50, 244, 127, 26, 254, 184, 102, 74, 11, 52>>, "session", ~U[2021-10-06 22:13:44.110158Z]]
**[info] GET /users/log_in**
[debug] Processing with MyappWeb.UserSessionController.new/2
Parameters: %{}
Pipelines: [:browser, :redirect_if_user_is_authenticated]
[info] Sent 200 in 6ms
Notice how in the logs above, the 302 redirect occurs, and then immediately the socket reconnects and mount/3
runs, which then triggers another redirect, this time to the /users/log_in
route. As far as I understand, the socket should not be trying to reconnect here, and I can't see what's triggering this.
Why is the PageLive
mount being triggered again after the 302 redirect to a non-liveview page upon logout, thus triggering a second redirect to the login page?