Phoenix auth generators create useful auth functions in the UserAuth
module that can be used to restrict entire live_session
blocks in the router or an individual LiveView to authenticated users. By putting calling them in mount hooks, they can ensure unauthenticated users are redirected during the mount phase of the Phoenix LiveView life cycle, before the page is rendered.
on_mount {AppWeb.UserAuth, :require_authenticated}
However, in some cases, some actions in a LiveView require authentication and others do not. For these circumstances we could call functions in UserAuth manually to check for an authenticated user, but it's better to make the view secure by default.
Creating our own function for finer-grained control
The mount function we'll build will be used in LiveViews like this:
on_mount {AppWeb.UserAuth, {:require_authenticated_except, actions_always_allowed}}
To test it, we'll make two my hypothetical versions of an index page—a "graphical" view of it and an "augmented" one. First we'll add links in the header of our generated LevelLive.Index page's HEEX template:
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<.header>
Listing Levels
<div class="text-sm text-gray-400">
(
<.link navigate={~p"/levels/"}>Default</.link> |
<.link navigate={~p"/levels/graphical"}>Graphical</.link> |
<.link navigate={~p"/levels/augmented"}>Augmented</.link>
)
</div>
<:actions :if={@current_scope}>
<.button variant="primary" navigate={~p"/levels/new"}>
<.icon name="hero-plus" /> New Level
</.button>
</:actions>
</.header>
Then in the router, we'll need to add the two new routes and send them to the index LiveView.
pipe_through [:browser]
live_session :current_user,
on_mount: [{AppWeb.UserAuth, :mount_current_scope}] do
live "/levels", LevelLive.Index, :index
live "/levels/graphical", LevelLive.Index, :graphical
live "/levels/augmented", LevelLive.Index, :augmented
live "/levels/new", LevelLive.Form, :new
live "/levels/:id", LevelLive.Show, :show
live "/levels/:id/edit", LevelLive.Form, :edit
live "/users/register", UserLive.Registration, :new
live "/users/log-in", UserLive.Login, :new
live "/users/log-in/:token", UserLive.Confirmation, :new
end
Writing the conditional authentication hook
In user_auth.ex
using the on_mount(:require_authenticated, _params, session, socket)
function definition as a model, create a conditional version that doesn't require authentication for a list of allowed actions:
def on_mount({:require_authenticated_except, allowed_actions}, _params, session, socket) do
socket = mount_current_scope(socket, session)
current_user = socket.assigns.current_scope && socket.assigns.current_scope.user
action_always_allowed = socket.assigns.live_action in allowed_actions
if current_user || action_always_allowed do
{:cont, socket}
else
socket =
socket
|> Phoenix.LiveView.put_flash(:error, "You must log in to access this page.")
|> Phoenix.LiveView.redirect(to: ~p"/users/log-in")
{:halt, socket}
end
end
Testing it in the Index LiveView
Near the top of the module, add the new hook and and set it to require authentication except for index and graphical actions:
defmodule AppWeb.LevelLive.Index do
use AppWeb, :live_view
alias App.Curriculum
on_mount {AppWeb.UserAuth, {:require_authenticated_except, [:index, :graphical]}}
Now, when clicking on the "Default" and "Graphical" links in the header, the index page will load at the appropriate route, for all users, but clicking "Augmented" will redirect unauthenticated users to the login page.
Final points
When possible, it's a good idea to make an entire LiveView require authentication (or not). However, cases where a single view has mixed authentication requirements depending on the action, this pattern is easy to implement, makes it easy to make changes in auth rules and is secure by default.
No Comments