Subscribe to access all episodes. View plans →
Published July 4, 2022
Elixir 1.12
Phoenix 1.6
LiveView 0.17.10
Follow along with the episode starter on GitHub
Here we have an application that’s using Phoenix LiveView to display different albums.
We can perform different actions on our albums and the links to perform these actions are public, which allows anyone to edit or even delete an album.
In this episode let’s add authentication so that users need to authenticate to access our site. Our app here is using LiveView and even though LiveView runs on the same core as Phoenix, it relies on a WebSocket connection, instead of the traditional HTTP request-response cycle.
Because of this, the authentication pattern will be slightly different than what was covered in episode 122. But just like in that episode we’ll be using Phoenix’s built-in authentication framework to get started. Let’s go to the command line and run mix phx.gen.auth
with an Accounts
context, and a User
schema with users
for the database table.
$ mix phx.gen.auth Accounts User users
...
Because this adds a new dependency we’ll need to re-fetch our dependencies.
$ mix deps.get
...
And then we can run the migration it generated for us:
$ mix ecto.migrate
...
Now let’s open our router.ex
and phoenix added some things to this file for us. It added the fetch_current_user
plug to the browser
pipeline, which will fetch the current user if their information is available.
Then if we scroll down, some authentication routes were added for us. These routes are being sent through some new plugs - the redirect_if_user_is_authenticated
plug which will redirect our users if they are authenticated. And the require_authenticated_user
plug requires a user to be authenticated to access one of these routes here. We want our users to be authenticated before accessing our album routes, so let’s use this plug to secure those routes.
We’ll go up to our scope that contains our album routes and add the require_authenticated_user
plug.
lib/teacher_web/router.ex
...
scope "/", TeacherWeb do
pipe_through [:browser, :require_authenticated_user]
live "/albums/new", AlbumLive.Index, :new
live "/albums/:id/edit", AlbumLive.Index, :edit
live "/albums/:id", AlbumLive.Show, :show
live "/", AlbumLive.Index, :index
end
...
Now let’s test this out. We’ll go to the command line and start our server.
$ mix phx.server
...
We’re immediately redirected to the sign-in page. If we try to access the album index page we’re redirected again to the “Log in” page with a message. Let’s go ahead and create an account. Great, once we’re authenticated we can access the previously restricted pages of our application. This is great, but as we can see the user menu is outside of our page’s container here, let’s update this to display in the container here. Let’s open the root.html.heex
layout. When we ran the auth generator, it added the _user_menu.html
partial template to our layout here. This partial contains those user links we want to move. If we open that template and we can see it checks if there’s a @current_user
- which is set by the fetch_current_user
plug we saw earlier in our router - and uses that to determine what links to display. So where should we move this template?
When working with LiveView, we typically have 3 layouts to consider - the root.html.heex
layout, which is used by both LiveView and regular views. Then there’s the app.html.heex
layout, which is the default application layout and not used by LiveView. And the live.html.heex
layout, which is rendered as part of the LiveView life-cycle. Because all our pages are handled by LiveView, let’s move the _user_menu.html
partial here, removing it from the root.html.heex
layout.
Let’s go back to the browser and when we do we get an error. The @current_user
doesn’t exist in our assigns when we call it in the _user_menu.html
partial. This comes back to the differences between Phoenix and LiveView. Phoenix operates through HTTP - when you load a page with Phoenix through HTTP the conn
struct is invoked and used - this is where the fetch_current_user
plug put the authenticated user, and why the page worked before.
Now that we’ve moved our partial to the live.html.heex
layout, which is used by Phoenix LiveView, and instead of HTTP and its @conn
struct, LiveView uses WebSockets and a @socket
struct. The data and assigns are not shared between the @conn
and @socket
so we’ll need to find a way to update our application to load the user and set it on the socket assigns. When a user logs in to our app, the auth plug stores a user_token
in the session. We can use that to look up the current user and assign it to the socket. To do that let’s create a new module in the “teacher_web” directory named live_auth.ex
. In this module, we’ll want to do a couple things. First, we’ll need to get the user from the session token if it exists. Then we’ll need to check to see if there is a user. If the user exists, we’ll add the user to the socket and continue. If no user exists, we’ll redirect them to sign in.
To handle this we’ll use the Phoenix.LiveView.on_mount
function, which lets us declare a callback that will be invoked when our LiveView mounts. Let’s define our function and pattern match on :require_authenticated_user
. Then it will take our “params” which we can ignore since we won’t need them, the “session”, and the “socket”.
The first thing we’ll do is create a way to assign the current user to the socket, so let’s define a private function named assign_current_user
that we’ll call from on_mount
, it will take the socket and then the session. Inside the function let’s use a case
statement to pattern match the session.
If the “user_token” key exists, let’s grab it and then use it to fetch our user from the database, and put them assign them to the socket as current_user
, which our template expects - using the assign_new
function. Let’s alias our Teacher.Accounts
module so we can call it without the prefix. Then if there’s no “user_token” in the session, we’ll just set the current_user
to nil.
Alright, let’s get our updated socket
in the on_mount
above. Now we can check if a user exists or not. We’ll use another case
statement and if the current_user
returns nil
we don’t want to continue so we’ll return :halt
tuple, with our socket, marked up with a flash message letting the user know they need to sign in to continue. And then we’ll redirect them to the sign-in page. We’ll add an alias for our routes helper too. Then if the user exists we’ll pattern match on the %User{}
struct and we want to continue so we’ll return a :cont
tuple with our socket. We’ll add another alias for our User
module.
lib/teacher_web/live_auth.ex
defmodule TeacherWeb.LiveAuth do
import Phoenix.LiveView
alias Teacher.Accounts
alias Teacher.Accounts.User
alias TeacherWeb.Router.Helpers, as: Routes
def on_mount(:require_authenticated_user, _, session, socket) do
socket = assign_current_user(socket, session)
case socket.assigns.current_user do
nil ->
{:halt,
socket
|> put_flash(:error, "You have to Sign in to continue")
|> redirect(to: Routes.user_session_path(socket, :new))}
%User{} ->
{:cont, socket}
end
end
defp assign_current_user(socket, session) do
case session do
%{"user_token" => user_token} ->
assign_new(socket, :current_user, fn ->
Accounts.get_user_by_session_token(user_token)
end)
%{} ->
assign_new(socket, :current_user, fn -> nil end)
end
end
end
Now we need to add this to our LiveView. Then let’s open the index.ex
LiveView module since this is the LiveView we were trying to access when we got the error. And in the module, let’s invoke the on_mount
we just created, which will be invoked on the LiveView’s mount.
lib/teacher_web/live/album_live/index.ex
defmodule TeacherWeb.AlbumLive.Index do
...
on_mount {TeacherWeb.LiveAuth, :require_authenticated_user}
...
end
We’ll need to make one other change. Let’s go back to that _user_menu.html.heex
template. Because it’s now being used from a LiveView, we’ll change the @conn
to use the @socket
.
Template path: lib/teacher_web/templates/layout/_user_menu.html.heex
<ul>
<%= if @current_user do %>
<li><%= @current_user.email %></li>
<li><%= link "Settings", to: Routes.user_settings_path(@socket, :edit) %></li>
<li><%= link "Log out", to: Routes.user_session_path(@socket, :delete), method: :delete %></li>
<% else %>
<li><%= link "Register", to: Routes.user_registration_path(@socket, :new) %></li>
<li><%= link "Log in", to: Routes.user_session_path(@socket, :new) %></li>
<% end %>
</ul>
Great, now let’s go back to the browser and if we reload the browser - it works - our page is working and our user menu is being displayed inside our container! With our current approach whenever we want to trigger our on_mount
we would need to invoke it in each LiveView we want to restrict access to. But there’s another approach we can take with the LiveView.Router.live_session
function, which will allow us to group live routes together, and have our on_mount
called for each one.
There is a note about security in the docs as a reminder to always perform your authentication and authorization in your LiveViews and if your application handles both HTTP requests and LiveViews, you need to perform these on both because live_redirect
s don’t go through the plug pipeline. Let’s go ahead and remove the on_mount
from our LiveView and then open the router.
Here we can wrap all the routes we want a user to be authenticated to access with the live_session
we’ll give it a name :require_auth
and then the on_mount
giving it our LiveAuth hook module, and then the name to pattern match on require_authenticated_user
. With that now our LiveAuth is will be invoked with the mount lifecycle of each LiveView in this block.
lib/teacher_web/router.ex
...
scope "/", TeacherWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :require_auth, on_mount: [{TeacherWeb.LiveAuth, :require_authenticated_user}] do
live "/albums/new", AlbumLive.Index, :new
live "/albums/:id/show/edit", AlbumLive.Show, :edit
live "/albums/:id", AlbumLive.Show, :show
live "/", AlbumLive.Index, :index
end
end
...
Now when we go to the browser again…we can view our pages….so let’s try signing out and then accessing our pages and we’re redirected. Our authentication with LiveView is now working and putting our user on the socket.