Skip to content

Instantly share code, notes, and snippets.

@kieraneglin
Last active February 11, 2024 03:44
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kieraneglin/819ccc802016e4040e0aa11e80ff20d7 to your computer and use it in GitHub Desktop.
Save kieraneglin/819ccc802016e4040e0aa11e80ff20d7 to your computer and use it in GitHub Desktop.
Pow sessions with LiveView (including tests)
defmodule MyAppWeb.ExampleLiveTest do
# `LiveviewCase` is a custom test helper - pretty much the same as ConnCase but with
# import Phoenix.LiveViewTest
# import MyApp.Support.AuthHelpers
use MyAppWeb.LiveviewCase, async: false
import MyApp.Factory
alias MyApp.Repo
alias MyAppWeb.ExampleLive
describe "mount" do
test "Some contrived example", %{conn: conn} do
user = insert(:user)
# It's important to feed in a `conn` from `Phoenix.ConnTest.build_conn()`
# (which is passed automatically to tests when using ConnCase). If you
# get a `Plug.MissingAdapter.send_resp` error, this is why
{:ok, view, html} = live_isolated(set_user_session(conn, user), ExampleLive)
# ...
end
end
end
# Adapted from https://dev.to/oliverandrich/how-to-connect-pow-and-live-view-in-your-phoenix-project-1ga1
# Usage https://hexdocs.pm/phoenix_live_view/security-model.html#mounting-considerations
# Don't forget to replace all occurances of `:my_app` with your app name
defmodule MyAppb.Views.Helpers.LiveHelpers do
import Phoenix.LiveView
alias Pow.Store.CredentialsCache
alias Pow.Store.Backend.EtsCache
@doc """
Fetches current user details from session, if present
"""
def assign_defaults(socket, session) do
assign_new(socket, :current_user, fn -> get_user(socket, session) end)
end
defp get_user(socket, session, config \\ [otp_app: :my_app])
defp get_user(socket, %{"my_app_auth" => signed_token}, config) do
conn = struct!(Plug.Conn, secret_key_base: socket.endpoint.config(:secret_key_base))
salt = Atom.to_string(Pow.Plug.Session)
with {:ok, token} <- Pow.Plug.verify_token(conn, salt, signed_token, config),
{user, _metadata} <- CredentialsCache.get([backend: EtsCache], token) do
user
else
_ -> nil
end
end
defp get_user(_, _, _), do: nil
end
defmodule MyAppWeb.Views.Helpers.LiveHelpersTest do
# NOTE: requires `get_session_token` fn as specified in `test_auth_helpers.ex`
use MyAppWeb.LiveviewCase
import MyApp.Factory
alias Phoenix.LiveView.Socket
alias MyAppWeb.Views.Helpers.LiveHelpers
describe "assign_defaults" do
test "Puts the current user in assigns if session data exists", %{conn: conn} do
user = insert(:user)
socket = %Socket{endpoint: MyAppWeb.Endpoint}
session = %{"myapp_auth" => get_session_token(conn, user)}
%{assigns: assigns} = LiveHelpers.assign_defaults(socket, session)
assert assigns.current_user.id == user.id
assert assigns.current_user.__meta__.state == :loaded
end
end
end
defmodule MyApp.Support.AuthHelpers do
@doc """
This is for setting the `APPNAME_auth` session for things like LiveView tests
Always feed in a conn from ConnCase or LiveviewCase, otherwise you may get a
`Plug.MissingAdapter.send_resp` error.
If transient failures appear, consider adding `:timer.sleep(100)` at the end of the fn
so the write to the storage adapter can complete. Doesn't seem to be needed but
some people say it's required. I don't know enough about Pow internals to confirm
"""
def set_user_session(conn, user) do
opts = Pow.Plug.Session.init(otp_app: :myapp)
%Plug.Conn{conn | secret_key_base: MyAppWeb.Endpoint.config(:secret_key_base)}
|> Pow.Plug.put_config(otp_app: :myapp)
|> Plug.Test.init_test_session(%{})
|> Pow.Plug.Session.call(opts)
|> Pow.Plug.Session.do_create(user, opts)
end
# --------------
# OR if you just need the token
# --------------
@doc """
Returns a valid session token for a given user
If transient failures appear, consider adding :timer.sleep(100) at the end of the fn
"""
def get_session_token(conn, user) do
pow_config = Keyword.put(Application.get_env(:myapp, :pow), :plug, Pow.Plug.Session)
metadata = [inserted_at: :os.system_time(:millisecond), fingerprint: "fingerprint"]
Pow.Store.CredentialsCache.put(
pow_config,
"session_id",
{user, metadata}
)
%Plug.Conn{conn | secret_key_base: MyAppWeb.Endpoint.config(:secret_key_base)}
|> Pow.Plug.sign_token(Atom.to_string(Pow.Plug.Session), "session_id", [])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment