From elixir-phoenix-guide
Implements Phoenix LiveView authentication patterns using on_mount hooks, mount_current_scope for session handling, and live_session router blocks. Invoke before writing auth logic in LiveViews.
npx claudepluginhub j-morgan6/elixir-phoenix-guide --plugin elixir-phoenix-guideThis skill uses the workspace's default tool permissions.
1. **Always use `on_mount` callbacks for LiveView auth** — never check auth in `mount/3` directly; `on_mount` runs before mount and centralizes auth logic
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
on_mount callbacks for LiveView auth — never check auth in mount/3 directly; on_mount runs before mount and centralizes auth logicmount_current_scope/2 to extract scope from session — never access session tokens manually or parse session data in LiveViews:cont and :halt returns from on_mount — :halt must redirect with a flash message, never silently drop the connectionPhoenix.Controller and Phoenix.LiveView both export redirect/2 and put_flash/3; use except: to avoid ambiguityassigns[:current_scope] in templates — dot access @current_scope crashes on nil when user is not authenticated{:error, {:redirect, %{to: path}}} — don't test auth by checking rendered content; verify the redirect tuple from live/2on_mount hooks once, reference via live_session in router — never duplicate auth logic across LiveView modulesThe standard pattern for LiveView authentication. Define once, use everywhere via live_session.
defmodule MyAppWeb.UserAuth do
use MyAppWeb, :verified_routes
import Phoenix.LiveView
import Phoenix.Controller, except: [redirect: 2, put_flash: 3]
# Called by live_session :require_authenticated_user
def on_mount(:require_authenticated_user, _params, session, socket) do
socket = mount_current_scope(socket, session)
if socket.assigns.current_scope && socket.assigns.current_scope.user do
{:cont, socket}
else
socket =
socket
|> put_flash(:error, "You must log in to access this page.")
|> redirect(to: ~p"/users/log_in")
{:halt, socket}
end
end
# Called by live_session :redirect_if_authenticated
def on_mount(:redirect_if_authenticated, _params, session, socket) do
socket = mount_current_scope(socket, session)
if socket.assigns.current_scope && socket.assigns.current_scope.user do
{:halt, redirect(socket, to: ~p"/")}
else
{:cont, socket}
end
end
# Called by live_session :mount_current_scope (public pages)
def on_mount(:mount_current_scope, _params, session, socket) do
{:cont, mount_current_scope(socket, session)}
end
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
if user = find_user_from_session(session) do
%Scope{user: user}
end
end)
end
defp find_user_from_session(%{"user_token" => token}) do
Accounts.get_user_by_session_token(token)
end
defp find_user_from_session(_session), do: nil
end
Use live_session to apply on_mount hooks to groups of LiveViews. Each session shares auth requirements.
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# Public pages — scope is mounted but not required
live_session :mount_current_scope,
on_mount: [{MyAppWeb.UserAuth, :mount_current_scope}] do
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive.Index
end
end
# Authenticated pages — redirects to login if not authenticated
live_session :require_authenticated_user,
on_mount: [{MyAppWeb.UserAuth, :require_authenticated_user}] do
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live "/dashboard", DashboardLive.Index
live "/settings", SettingsLive.Index
end
end
# Guest-only pages — redirects to home if already authenticated
live_session :redirect_if_authenticated,
on_mount: [{MyAppWeb.UserAuth, :redirect_if_authenticated}] do
scope "/", MyAppWeb do
pipe_through [:browser, :redirect_if_user]
live "/users/register", UserRegistrationLive
live "/users/log_in", UserLoginLive
end
end
end
Phoenix.Controller and Phoenix.LiveView both export redirect/2 and put_flash/3. When you need both in the same module (common in UserAuth):
# Bad — compile error or wrong function called
import Phoenix.Controller
import Phoenix.LiveView
# Good — explicitly exclude conflicting functions
import Phoenix.LiveView
import Phoenix.Controller, except: [redirect: 2, put_flash: 3]
# Now redirect/2 and put_flash/3 come from Phoenix.LiveView
Phoenix 1.8+ uses Scope structs instead of raw current_user. The scope wraps the user and can carry additional context.
# Phoenix 1.8+ pattern — Scope struct
defmodule MyApp.Scope do
defstruct [:user]
end
# In LiveView — access user through scope
def mount(_params, _session, socket) do
user = socket.assigns.current_scope.user
{:ok, assign(socket, :posts, Posts.list_posts(user))}
end
# In templates — use bracket access for safety
<%= if assigns[:current_scope] && @current_scope.user do %>
<p>Welcome, <%= @current_scope.user.email %></p>
<% end %>
Always use bracket access for assigns that may not exist (e.g., on public pages where auth is optional):
# Bad — crashes if current_scope is nil
<%= @current_scope.user.email %>
# Good — safe bracket access
<%= if assigns[:current_scope] && @current_scope.user do %>
<%= @current_scope.user.email %>
<% end %>
# Also good — assign_new with default
def on_mount(:mount_current_scope, _params, session, socket) do
{:cont, mount_current_scope(socket, session)}
end
describe "require_authenticated_user" do
test "redirects if not logged in", %{conn: conn} do
assert {:error, {:redirect, %{to: "/users/log_in"}}} =
live(conn, ~p"/dashboard")
end
test "renders page when authenticated", %{conn: conn} do
user = user_fixture()
conn = log_in_user(conn, user)
{:ok, _lv, html} = live(conn, ~p"/dashboard")
assert html =~ "Dashboard"
end
end
describe "redirect_if_authenticated" do
test "redirects if already logged in", %{conn: conn} do
user = user_fixture()
conn = log_in_user(conn, user)
assert {:error, {:redirect, %{to: "/"}}} =
live(conn, ~p"/users/log_in")
end
end
describe "on_mount: :require_authenticated_user" do
test "authenticates user from session", %{conn: conn} do
user = user_fixture()
token = Accounts.generate_user_session_token(user)
assert {:cont, updated_socket} =
UserAuth.on_mount(
:require_authenticated_user,
%{},
%{"user_token" => token},
%LiveView.Socket{
endpoint: MyAppWeb.Endpoint,
assigns: %{__changed__: %{}}
}
)
assert updated_socket.assigns.current_scope.user.id == user.id
end
test "redirects when no session token" do
assert {:halt, updated_socket} =
UserAuth.on_mount(
:require_authenticated_user,
%{},
%{},
%LiveView.Socket{
endpoint: MyAppWeb.Endpoint,
assigns: %{__changed__: %{}, flash: %{}}
}
)
assert updated_socket.redirected == {:redirect, %{to: "/users/log_in"}}
end
end
See testing-essentials skill for comprehensive testing patterns.
See phoenix-authorization-patterns skill for authorization after authentication.