Screencast

Set session values from LiveView

6. Set session values from LiveView

csrf encrypt session

In most web applications, there's a need to store session values for users, especially for things like authentication tokens, current user IDs, or preferences like language settings or dark mode. In a LiveView application, which primarily communicates over WebSockets, setting session values directly isn't possible. However, as demonstrated, when I click the "Set Session" button and refresh the page, the value remains accessible through the browser's session. In this video, I’ll show you how to achieve this seamlessly and elegantly.

Since session values can’t be set directly from LiveView, the first step is to create a controller that handles setting session values for the user. I’ll start by creating a new controller where session values can be posted as parameters. These parameters will be a map containing session keys and their corresponding values. I’ll iterate through the keys and set their values, ensuring that the keys are safely converted to existing atoms for security purposes.

Before I can use the controller, I need to set up a route for it. Depending on your application’s requirements, you might want to secure this route behind an authenticated pipeline. For now, I’ll address security in a different way, so I won’t add authentication to the route.

Even with the controller in place, we can’t post to it directly from LiveView. Instead, we’ll dispatch a JavaScript event within the LiveView that will handle the posting. In the app’s JavaScript file, I’ll add an event listener that listens for these events, takes the payload, and sends it to the new session storage route. It’s important to note that I’m using a CSRF token from the meta tag in the DOM to ensure that the post request is legitimate.

To demonstrate this setup, I’ve prepared a basic LiveView file with two buttons and their respective event handlers. When either button is clicked, the event handlers trigger, sending a push event with the name 'store-session' and a map containing the session key and value. This push event is caught by the JavaScript event handler added earlier. When the LiveView is mounted, it receives the session as one of its arguments, and for demonstration purposes, I’ll check for a session value called my_key.

Testing this setup with the development console open, you can see that when I click the button, a post request is sent to the correct URL with the parameters my_key and my_value.

Now that the basics are working, I want to add an extra layer of security. Since users can inspect session data sent from the browser console, we can use Phoenix’s built-in encryption functionality to secure this data. I’ll add an encrypt function that allows us to send the session map as an encrypted binary.

Next, I’ll update the controller to handle encrypted payloads. To accommodate both encrypted and non-encrypted parameters, I’ll use pattern matching in the function head. The logic is similar to the previous version, but now it attempts to decrypt the parameters first. The decrypt function mirrors the encrypt function used in LiveView. In a real-world application, you’d typically place these functions in a separate module and use a more complex secret key.

When I test this updated setup, it works just as before. However, inspecting the console now shows that the parameters are indeed encrypted, providing additional security.

With this approach, we’ve created a straightforward, secure, and reusable way to set user session values from Phoenix LiveView.

Code

lib/tutorial_web/controllers/store_session_controller.ex

defmodule TutorialWeb.StoreSessionController do
  use TutorialWeb, :controller

  def create(conn, %{"encrypted" => token}) do
    updated_conn =
      case decrypt(token) do
        {:ok, params} ->
          Enum.reduce(params, conn, fn {key, value}, acc_conn ->
            put_session(acc_conn, key, value)
          end)
        _ ->
          conn
      end

    send_resp(updated_conn, 200, "")
  end

  # Note, you must have defined the atom before.
  def create(conn, params) do
    updated_conn = Enum.reduce(params, conn, fn {key, value}, acc_conn ->
      put_session(acc_conn, String.to_existing_atom(key), value)
    end)

    send_resp(updated_conn, 200, "")
  end

  defp decrypt(token) do
    Phoenix.Token.decrypt(TutorialWeb.Endpoint, "secret_key", token, max_age: 600)
  end
end

assets/js/app.js

window.addEventListener("phx:store-session", (event) => {
  const token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')

  fetch('/store-session', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': token
    },
    body: JSON.stringify(event.detail)
  })
})

lib/tutorial_web/live/demo_live.ex

def handle_event("demo", _param, socket) do
  {
    :noreply,
    socket
    |> push_event("store-session",  %{encrypted: encrypt(%{my_key: "My Value"})})
  }
end

def handle_event("clear", _param, socket) do
  {
    :noreply,
    socket
    |> assign(:value_from_session, nil)
    |> push_event("store-session",  %{my_key: nil})
  }
end

defp encrypt(payload) do
  Phoenix.Token.encrypt(TutorialWeb.Endpoint, "secret_key", payload)
end