Skip to main content
LiveView begins its life-cycle as a regular HTTP request, then establishes a stateful connection. Both phases receive client data via parameters and session, requiring careful security considerations.
Any session validation must happen both in the HTTP request (plug pipeline) and the stateful connection (LiveView mount).

Authentication vs Authorization

Authentication identifies who a user is (e.g., by email/password or OAuth). Authorization determines what a user can access or do in the system.

Authentication Flow

1

User logs in

User provides credentials (email/password, OAuth, etc.).
2

Token stored in session

A token identifying the user is stored in the session cookie.
3

Session validated on each request

For each request, the session is read and validated.
4

User fetched from database

If valid, the user is fetched from the database.
Phoenix and tools like mix phx.gen.auth can generate the building blocks of an authentication system for you.

LiveView Security Flow

LiveViews share authentication logic with regular requests through plugs:
  1. HTTP Request → Endpoint → Router → Plugs → LiveView HTTP mount
  2. WebSocket Connection → LiveView connected mount
Both mount phases must validate the session independently.

Using live_session

The Phoenix.LiveView.Router.live_session/2 is the primary mechanism for grouping LiveViews with shared authentication/authorization requirements.

Basic Usage

scope "/" do
  pipe_through [:authenticate_user]

  live_session :default, on_mount: MyAppWeb.UserLiveAuth do
    live "/", HomeLive
    live "/dashboard", DashboardLive
  end
end

scope "/admin" do
  pipe_through [:authenticate_admin]

  live_session :admin, on_mount: MyAppWeb.AdminLiveAuth do
    live "/admin", AdminLive
    live "/admin/users", AdminUsersLive
  end
end

Why live_session?

  • Navigation within a session: Skip regular HTTP requests and plug pipeline
  • Navigation across sessions: Go through the router and plugs again
  • Security boundary: Different authentication requirements
  • Layout changes: Different root layouts for different sections
Use live_session to enforce different authentication requirements (like user vs admin), not just for minor authorization differences.

The on_mount Hook

The on_mount hook allows you to run authentication/authorization logic on every LiveView mount.

Implementing a LiveAuth Hook

defmodule MyAppWeb.UserLiveAuth do
  import Phoenix.Component
  import Phoenix.LiveView
  alias MyAppWeb.Accounts  # from `mix phx.gen.auth`

  def on_mount(:default, _params, %{"user_token" => user_token} = _session, socket) do
    socket =
      assign_new(socket, :current_user, fn ->
        Accounts.get_user_by_session_token(user_token)
      end)

    if socket.assigns.current_user.confirmed_at do
      {:cont, socket}
    else
      {:halt, redirect(socket, to: "/login")}
    end
  end
  
  def on_mount(:default, _params, _session, socket) do
    {:halt, redirect(socket, to: "/login")}
  end
end
Use assign_new/3 to avoid fetching the current user multiple times across parent-child LiveViews.

Using the Hook

You can use the hook in three ways: 1. In the router (recommended):
live_session :default, on_mount: MyAppWeb.UserLiveAuth do
  live "/dashboard", DashboardLive
end
2. Directly in the LiveView:
defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view
  on_mount MyAppWeb.UserLiveAuth
  
  # ...
end
3. In all LiveViews by default:
# lib/my_app_web.ex
def live_view do
  quote do
    use Phoenix.LiveView
    on_mount MyAppWeb.UserLiveAuth
    unquote(html_helpers())
  end
end

Mounting Considerations

The mount/3 callback is invoked both on initial HTTP mount and when LiveView connects. Any authorization performed during mount covers all scenarios.

Example: User Authentication

def mount(_params, %{"user_id" => user_id} = _session, socket) do
  socket = assign(socket, current_user: Accounts.get_user!(user_id))

  socket =
    if socket.assigns.current_user.confirmed_at do
      socket
    else
      redirect(socket, to: "/login")
    end

  {:ok, socket}
end

Example: Resource Authorization

def mount(%{"org_id" => org_id}, _session, socket) do
  user = socket.assigns.current_user
  
  # Only get orgs that belong to the user
  organizations_query = Ecto.assoc(user, :organizations)
  org = Repo.get!(organizations_query, org_id)
  
  {:ok, assign(socket, org: org)}
end
If the user doesn’t have access to the organization, Repo.get! raises Ecto.NoResultsError, which is converted to a 404 page.

Event Authorization

Every user action must be verified on the server, regardless of UI state.

Why Event Authorization?

Even if you hide the “Delete” button in the UI, a savvy user can directly send a delete event to the server. You must always verify permissions on the server.

Example: Delete Authorization

defmodule MyAppWeb.ProjectsLive do
  use MyAppWeb, :live_view
  on_mount MyAppWeb.UserLiveAuth

  def mount(_params, _session, socket) do
    {:ok, load_projects(socket)}
  end

  def handle_event("delete_project", %{"project_id" => project_id}, socket) do
    # Always pass current_user to verify permissions
    Project.delete!(socket.assigns.current_user, project_id)
    {:noreply, update(socket, :projects, &Enum.reject(&1, fn p -> p.id == project_id end))}
  end

  defp load_projects(socket) do
    projects = Project.all_projects(socket.assigns.current_user)
    assign(socket, projects: projects)
  end
end
In your context:
defmodule MyApp.Project do
  def delete!(user, project_id) do
    project = Repo.get!(Project, project_id)
    
    if can_delete?(user, project) do
      Repo.delete!(project)
      true
    else
      raise "Unauthorized"
    end
  end
  
  defp can_delete?(user, project) do
    project.owner_id == user.id || user.role == :admin
  end
end
Always verify permissions on the serverNever trust client-side UI state for authorization. Users can send events directly to your server.

Disconnecting Live Users

Because LiveView maintains a permanent connection, changes like logout or account revocation won’t reflect until the user reloads the page.

Using live_socket_id

Set a live_socket_id in the session when logging in:
conn
|> put_session(:current_user_id, user.id)
|> put_session(:live_socket_id, "users_socket:#{user.id}")
Now all LiveView sockets are identified by this ID. Disconnect all live users with:
MyAppWeb.Endpoint.broadcast("users_socket:#{user.id}", "disconnect", %{})
If you use mix phx.gen.auth, lines for this are already present in the generated code, using a user_token instead of user_id.

When to Disconnect

  • User logs out
  • User account is deleted
  • User permissions are revoked
  • User password is changed
  • Session is invalidated

How It Works

1

Broadcast disconnect

Server broadcasts “disconnect” message to the live_socket_id.
2

LiveView disconnects

All LiveView connections with that ID are disconnected.
3

Client attempts reconnection

The client automatically tries to reconnect.
4

Mount fails or redirects

If the user is no longer logged in, mount fails or redirects to login.

Shared with Phoenix Channels

This is the same mechanism provided by Phoenix.Channel, so it works for both LiveViews and Channels.

Complete Example

Router

scope "/" do
  pipe_through [:browser, :redirect_if_authenticated]
  
  live "/login", LoginLive
  live "/register", RegisterLive
end

scope "/" do
  pipe_through [:browser, :require_authenticated_user]

  live_session :authenticated, on_mount: MyAppWeb.UserLiveAuth do
    live "/", HomeLive
    live "/dashboard", DashboardLive
    live "/projects", ProjectsLive
  end
end

scope "/admin" do
  pipe_through [:browser, :require_admin_user]

  live_session :admin,
    on_mount: MyAppWeb.AdminLiveAuth,
    root_layout: {MyAppWeb.Layouts, :admin} do
    live "/admin", AdminDashboardLive
    live "/admin/users", AdminUsersLive
  end
end

UserLiveAuth Hook

defmodule MyAppWeb.UserLiveAuth do
  import Phoenix.LiveView
  import Phoenix.Component

  def on_mount(:default, _params, %{"user_token" => user_token}, socket) do
    socket = assign_new(socket, :current_user, fn ->
      MyAppWeb.Accounts.get_user_by_session_token(user_token)
    end)

    case socket.assigns.current_user do
      nil ->
        {:halt, redirect(socket, to: "/login")}
      
      %{confirmed_at: nil} ->
        {:halt, redirect(socket, to: "/confirm")}
      
      user ->
        {:cont, socket}
    end
  end

  def on_mount(:default, _params, _session, socket) do
    {:halt, redirect(socket, to: "/login")}
  end
end

AdminLiveAuth Hook

defmodule MyAppWeb.AdminLiveAuth do
  import Phoenix.LiveView
  import Phoenix.Component

  def on_mount(:default, _params, %{"user_token" => user_token}, socket) do
    socket = assign_new(socket, :current_user, fn ->
      MyAppWeb.Accounts.get_user_by_session_token(user_token)
    end)

    if socket.assigns.current_user && socket.assigns.current_user.role == :admin do
      {:cont, socket}
    else
      {:halt, redirect(socket, to: "/")}
    end
  end

  def on_mount(:default, _params, _session, socket) do
    {:halt, redirect(socket, to: "/login")}
  end
end

Best Practices

  1. Use live_session for authentication boundaries: Different user types (user vs admin) should have separate live sessions
  2. Implement on_mount hooks: Centralize authentication logic in reusable hooks
  3. Verify in events: Always check permissions in handle_event callbacks
  4. Set live_socket_id: Enable disconnecting users when their session is invalidated
  5. Use assign_new: Avoid redundant database queries across parent-child LiveViews
  6. Match on plug logic: Ensure LiveView auth matches your plug auth logic
  7. Test auth paths: Test both authenticated and unauthenticated scenarios
  8. Use context functions: Keep authorization logic in your context modules

Security Checklist

  • Authentication logic exists in both plugs and on_mount
  • live_session groups separate auth requirements
  • on_mount hooks validate user session
  • Resource access is verified in mount/3
  • Action permissions are checked in handle_event/3
  • live_socket_id is set for disconnecting users
  • CSRF tokens are validated (automatic in Phoenix)
  • Session data is signed and encrypted (automatic in Phoenix)
  • Authorization tests cover all scenarios

Summary

The important concepts:
  • live_session: Draw boundaries between groups of LiveViews with different authentication requirements
  • Authentication in mount: Shared between regular web requests (via plugs) and LiveViews (via on_mount)
  • Authorization in mount: Check if user can see this page
  • Authorization in events: Check if user can perform this action
  • Disconnect mechanism: Use live_socket_id to disconnect users when sessions are invalidated
These security requirements are the same for both regular web requests and LiveViews - LiveView simply provides convenient hooks and mechanisms to implement them.

Build docs developers (and LLMs) love