igmarin/elixir-phoenix-skills

phoenix-liveview-auth

MANDATORY for ALL LiveView authentication work. Invoke before writing on_mount hooks, auth plugs for LiveViews, or session handling in LiveView modules. Covers on_mount patterns, current_scope, import conflict resolution, safe template access, and testing auth redirects. Trigger words: on_mount, LiveView auth, current_scope, session, live_session, redirect_if_authenticated.

First seen Jun 20, 2026

Installation

$ npx skills add igmarin/elixir-phoenix-skills --skill phoenix-liveview-auth

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from igmarin/elixir-phoenix-skills · top by installs.

npx skills add igmarin/elixir-phoenix-skills

Browse all from igmarin/elixir-phoenix-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT
More metadata
version
1.0.0
user-invocable
true

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 7,338 B
  • docs SUMMARY.md 405 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 15 installs

SKILL.md

Phoenix LiveView Authentication

Use this skill before writing ANY on_mount hook or LiveView auth code.

Requires phoenix-scopes for Scope struct setup. See phoenix-authorization-patterns for access control after authentication, and phoenix-liveview-essentials before writing any LiveView module.

Canonical FP bar: [docs/fcis-engineering-rules.md](../../docs/fcis-engineering-rules.md) — Functional Core, Imperative Shell: pure domain modules; side effects at edges. Authorization decisions should be pure checks on scope/user data; persist only at the edge.

RULES — Follow these with no exceptions

1. Use onmount callbacks — never check auth in mount/3 directly 2. :halt must redirect with a flash message — never silently drop the connection 3. Define onmount hooks once, reference via livesession in router — never duplicate auth logic across LiveView modules 4. Resolve import conflicts with import Phoenix.Controller, except: [redirect: 2, putflash: 3] — so LiveView's redirect/2 and putflash/3 take precedence 5. Use bracket access assigns[:currentscope] in templates that can render unauthenticated — never @currentscope directly (raises KeyError) 6. Add @impl true to every LiveView callback — including a mount/3 that reads socket.assigns.currentscope.user

Implementation Workflow

  1. Define on_mount hooks in UserAuth with import conflict resolution; verify with mix compile
  2. Add live_session blocks to the router referencing those hooks; verify with mix phx.routes
  3. Run auth tests with mix test test/myappweb/live/ and assert redirect tuples match expected paths; if tests fail, check session config and verify Accounts.getuserbysessiontoken/1 returns the correct user
  4. Add template guards using bracket access for optional assigns

on_mount Authentication Pattern

defmodule MyAppWeb.UserAuth do
  use MyAppWeb, :verified_routes
  import Phoenix.LiveView
  # Exclude Phoenix.Controller's redirect/2 and put_flash/3 so LiveView's versions take precedence
  import Phoenix.Controller, except: [redirect: 2, put_flash: 3]

  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

  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

  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

See [assets/onmounttemplate.ex](assets/onmounttemplate.ex) for copy-paste onmount templates (Phoenix 1.7 currentuser, Phoenix 1.8+ Scope, and role-based variants).

Router Integration

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  live_session :mount_current_scope,
    on_mount: [{MyAppWeb.UserAuth, :mount_current_scope}] do
    scope "/", MyAppWeb do
      pipe_through :browser
      live "/", HomeLive.Index
    end
  end

  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

  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

Template Access

@impl true
def mount(_params, _session, socket) do
  user = socket.assigns.current_scope.user
  {:ok, assign(socket, :posts, Posts.list_posts(user))}
end
<%# Use assigns[:current_scope] (bracket access), not @current_scope — avoids KeyError on unauthenticated sockets %>
<%= if assigns[:current_scope] && @current_scope.user do %>
  <p>Welcome, <%= @current_scope.user.email %></p>
<% end %>

Testing LiveView Auth

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

Common Pitfalls

❌ Don't ✅ Do
Check auth inside mount/3 with a manual token lookup Use an onmount hook wired through livesession
Return {:halt, socket} with no redirect or flash redirect(socket, to: ~p"/users/login") plus a putflash/3
import Phoenix.Controller unfiltered (shadows LiveView helpers) import Phoenix.Controller, except: [redirect: 2, put_flash: 3]
Reference @current_scope in templates that render for guests Use assigns[:currentscope] && @currentscope.user (bracket access)
Repeat the auth check in every LiveView module Group routes under a live_session with the shared hook
Omit @impl true on LiveView callbacks Add @impl true above mount/3, handle_event/3, etc.

Integration

Predecessor This Skill Successor
phoenix-scopes phoenix-liveview-auth phoenix-authorization-patterns
phoenix-liveview-essentials phoenix-liveview-auth phoenix-auth-customization

Companion skills: phoenix-scopes, phoenix-authorization-patterns, testing-essentials