Skill v1.0.0
currentAutomated scan100/100version: "1.0.0" name: phoenix-liveview-auth description: MANDATORY for ALL LiveView authentication work. Invoke before writing on_mount hooks, auth plugs for LiveViews, or session handling in LiveView modules. file_patterns:
- "**/*_live.ex"
- "**/_live/.ex"
- "**/user_auth.ex"
auto_suggest: true
Phoenix LiveView Authentication
RULES — Follow these with no exceptions
- Always use `on_mount` callbacks for LiveView auth — never check auth in
mount/3directly;on_mountruns before mount and centralizes auth logic - Use `mount_current_scope/2` to extract scope from session — never access session tokens manually or parse session data in LiveViews
- Handle both `:cont` and `:halt` returns from `on_mount` —
:haltmust redirect with a flash message, never silently drop the connection - Resolve import conflicts explicitly —
Phoenix.ControllerandPhoenix.LiveViewboth exportredirect/2andput_flash/3; useexcept:to avoid ambiguity - Use bracket access `assigns[:current_scope]` in templates — dot access
@current_scopecrashes on nil when user is not authenticated - Test auth redirects by asserting `{:error, {:redirect, %{to: path}}}` — don't test auth by checking rendered content; verify the redirect tuple from
live/2 - Define `on_mount` hooks once, reference via `live_session` in router — never duplicate auth logic across LiveView modules
on_mount Authentication Pattern
The standard pattern for LiveView authentication. Define once, use everywhere via live_session.
defmodule MyAppWeb.UserAuth douse MyAppWeb, :verified_routesimport Phoenix.LiveViewimport Phoenix.Controller, except: [redirect: 2, put_flash: 3]# Called by live_session :require_authenticated_userdef on_mount(:require_authenticated_user, _params, session, socket) dosocket = mount_current_scope(socket, session)if socket.assigns.current_scope && socket.assigns.current_scope.user do{:cont, socket}elsesocket =socket|> put_flash(:error, "You must log in to access this page.")|> redirect(to: ~p"/users/log_in"){:halt, socket}endend# Called by live_session :redirect_if_authenticateddef on_mount(:redirect_if_authenticated, _params, session, socket) dosocket = mount_current_scope(socket, session)if socket.assigns.current_scope && socket.assigns.current_scope.user do{:halt, redirect(socket, to: ~p"/")}else{:cont, socket}endend# 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)}enddefp mount_current_scope(socket, session) doPhoenix.Component.assign_new(socket, :current_scope, fn ->if user = find_user_from_session(session) do%Scope{user: user}endend)enddefp find_user_from_session(%{"user_token" => token}) doAccounts.get_user_by_session_token(token)enddefp find_user_from_session(_session), do: nilend
Router Integration
Use live_session to apply on_mount hooks to groups of LiveViews. Each session shares auth requirements.
defmodule MyAppWeb.Router douse MyAppWeb, :router# Public pages — scope is mounted but not requiredlive_session :mount_current_scope,on_mount: [{MyAppWeb.UserAuth, :mount_current_scope}] doscope "/", MyAppWeb dopipe_through :browserlive "/", HomeLive.Indexendend# Authenticated pages — redirects to login if not authenticatedlive_session :require_authenticated_user,on_mount: [{MyAppWeb.UserAuth, :require_authenticated_user}] doscope "/", MyAppWeb dopipe_through [:browser, :require_authenticated_user]live "/dashboard", DashboardLive.Indexlive "/settings", SettingsLive.Indexendend# Guest-only pages — redirects to home if already authenticatedlive_session :redirect_if_authenticated,on_mount: [{MyAppWeb.UserAuth, :redirect_if_authenticated}] doscope "/", MyAppWeb dopipe_through [:browser, :redirect_if_user]live "/users/register", UserRegistrationLivelive "/users/log_in", UserLoginLiveendendend
Import Conflict Resolution
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 calledimport Phoenix.Controllerimport Phoenix.LiveView# Good — explicitly exclude conflicting functionsimport Phoenix.LiveViewimport Phoenix.Controller, except: [redirect: 2, put_flash: 3]# Now redirect/2 and put_flash/3 come from Phoenix.LiveView
current_scope vs current_user
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 structdefmodule MyApp.Scope dodefstruct [:user]end# In LiveView — access user through scopedef mount(_params, _session, socket) douser = 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 %>
Safe Template Access
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 defaultdef on_mount(:mount_current_scope, _params, session, socket) do{:cont, mount_current_scope(socket, session)}end
Testing LiveView Auth
Testing Protected Routes
describe "require_authenticated_user" dotest "redirects if not logged in", %{conn: conn} doassert {:error, {:redirect, %{to: "/users/log_in"}}} =live(conn, ~p"/dashboard")endtest "renders page when authenticated", %{conn: conn} douser = user_fixture()conn = log_in_user(conn, user){:ok, _lv, html} = live(conn, ~p"/dashboard")assert html =~ "Dashboard"endenddescribe "redirect_if_authenticated" dotest "redirects if already logged in", %{conn: conn} douser = user_fixture()conn = log_in_user(conn, user)assert {:error, {:redirect, %{to: "/"}}} =live(conn, ~p"/users/log_in")endend
Testing on_mount Directly
describe "on_mount: :require_authenticated_user" dotest "authenticates user from session", %{conn: conn} douser = 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.idendtest "redirects when no session token" doassert {: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"}}endend
See testing-essentials skill for comprehensive testing patterns. See phoenix-authorization-patterns skill for authorization after authentication.