Last active
November 11, 2024 19:15
-
-
Save kieraneglin/819ccc802016e4040e0aa11e80ff20d7 to your computer and use it in GitHub Desktop.
Pow sessions with LiveView (including tests)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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