Subscribe to access all episodes. View plans →

#147: Phoenix LiveView Authentication

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_redirects 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.

© 2024 HEXMONSTER LLC