This repository has been archived by the owner on Dec 12, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add helper to handle pow user in LiveViews.
The solution is based on suggestions in pow-auth/pow#271 and seems to tick all the boxes: - access the connected user in LiveViews. - (callback to) handle session expiration. - keeping sessions alive. This could be imprived by disconnecting all the current user LiveViews on sign out, but this would require custom Pow controllers. A at-most 60 seconds delay seems to be a good compromise for the time being.
- Loading branch information
1 parent
8897569
commit 7836566
Showing
14 changed files
with
392 additions
and
11 deletions.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
defmodule PixelForumWeb.Live.AuthHelper do | ||
@moduledoc """ | ||
This module helps assigning the current user from the session. Doing so will | ||
setup periodical checks that the session is still active. `session_expired/1` | ||
will be called when session expires. | ||
Will not renew the session though, this has to be done separately, by | ||
periodically fetching a dummy page from for instance. | ||
Configuration options: | ||
* `:otp_app` - the app name | ||
* `:interval` - how often the session has to be checked, defaults 60s | ||
defmodule MyAppWeb.Live.SomeViewLive do | ||
use MyAppWeb, :live_view | ||
use MyAppWeb.Live.AuthHelper, otp_app: my_app | ||
def mount(params, session, socket) do | ||
socket = maybe_assign_current_user(socket, session) | ||
# or | ||
socket = assign_current_user!(socket, session) | ||
# ... | ||
end | ||
def session_expired(socket) do | ||
# handle session expiration | ||
{:noreply, socket} | ||
end | ||
end | ||
""" | ||
require Logger | ||
|
||
alias Pow.Config | ||
|
||
import Phoenix.LiveView, only: [assign: 3] | ||
|
||
@callback session_expired(Phoenix.LiveView.Socket.t()) :: | ||
{:noreply, Phoenix.LiveView.Socket.t()} | ||
|
||
defmacro __using__(opts) do | ||
config = [otp_app: opts[:otp_app]] | ||
session_token = Pow.Plug.prepend_with_namespace(config, "auth") | ||
default_interval_duration = if Mix.env() == :test, do: 100, else: :timer.seconds(60) | ||
interval = Keyword.get(opts, :interval, default_interval_duration) | ||
cache_store_backend = Pow.Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache) | ||
|
||
config = [ | ||
session_token: session_token, | ||
interval: interval, | ||
cache_store_backend: cache_store_backend | ||
] | ||
|
||
quote do | ||
@behaviour unquote(__MODULE__) | ||
|
||
@config unquote(Macro.escape(config)) ++ [module: __MODULE__] | ||
|
||
def maybe_assign_current_user(socket, session), | ||
do: unquote(__MODULE__).maybe_assign_current_user(socket, self(), session, @config) | ||
|
||
def assign_current_user!(socket, session), | ||
do: unquote(__MODULE__).assign_current_user!(socket, self(), session, @config) | ||
|
||
def handle_info(:pow_auth_ttl, socket), | ||
do: unquote(__MODULE__).handle_auth_ttl(socket, self(), @config) | ||
end | ||
end | ||
|
||
@spec maybe_assign_current_user(Phoenix.LiveView.Socket.t(), pid(), map(), Config.t()) :: | ||
Phoenix.LiveView.Socket.t() | ||
def maybe_assign_current_user(socket, pid, session, config) do | ||
user_session_token = get_user_session_token(socket, session, config) | ||
user = get_current_user(user_session_token, config) | ||
|
||
# Start the interval check to see if the current user is still connected. | ||
init_auth_check(socket, pid, config) | ||
|
||
socket | ||
|> assign_current_user_session_token(user_session_token, config) | ||
|> assign_current_user(user, config) | ||
end | ||
|
||
@spec assign_current_user!(Phoenix.LiveView.Socket.t(), pid(), map(), Config.t()) :: | ||
Phoenix.LiveView.Socket.t() | ||
def assign_current_user!(socket, pid, session, config) do | ||
socket = maybe_assign_current_user(socket, pid, session, config) | ||
assign_key = Pow.Config.get(config, :current_user_assigns_key, :current_user) | ||
|
||
if is_nil(socket.assigns[assign_key]) do | ||
raise "There is no current user in the session." | ||
end | ||
|
||
socket | ||
end | ||
|
||
# Initiates an Auth check every :interval. | ||
defp init_auth_check(socket, pid, config) do | ||
interval = Pow.Config.get(config, :interval) | ||
|
||
if Phoenix.LiveView.connected?(socket) do | ||
Process.send_after(pid, :pow_auth_ttl, interval) | ||
end | ||
end | ||
|
||
# Called on interval when receiving :pow_auth_ttl. | ||
@spec handle_auth_ttl(Phoenix.LiveView.Socket.t(), pid(), Config.t()) :: | ||
{:noreply, Phoenix.LiveView.Socket.t()} | ||
def handle_auth_ttl(socket, pid, config) do | ||
interval = Pow.Config.get(config, :interval) | ||
module = Pow.Config.get(config, :module) | ||
session_token = get_current_user_session_token(socket, config) | ||
|
||
case get_current_user(session_token, config) do | ||
nil -> | ||
Logger.debug("[#{__MODULE__}] User session no longer active") | ||
|
||
socket | ||
|> assign_current_user_session_token(nil, config) | ||
|> assign_current_user(nil, config) | ||
|> module.session_expired() | ||
|
||
_user -> | ||
Logger.debug("[#{__MODULE__}] User session still active") | ||
Process.send_after(pid, :pow_auth_ttl, interval) | ||
{:noreply, socket} | ||
end | ||
end | ||
|
||
defp get_user_session_token(socket, session, config) do | ||
conn = struct!(Plug.Conn, secret_key_base: socket.endpoint.config(:secret_key_base)) | ||
salt = Atom.to_string(Pow.Plug.Session) | ||
|
||
with {:ok, signed_token} <- Map.fetch(session, config[:session_token]), | ||
{:ok, session_token} <- Pow.Plug.verify_token(conn, salt, signed_token, config) do | ||
session_token | ||
else | ||
_ -> nil | ||
end | ||
end | ||
|
||
defp assign_current_user(socket, user, config) do | ||
assign_key = Pow.Config.get(config, :current_user_assigns_key, :current_user) | ||
assign(socket, assign_key, user) | ||
end | ||
|
||
defp session_token_assign_key(config) do | ||
current_user_key = Pow.Config.get(config, :current_user_assigns_key, :current_user) | ||
String.to_atom("#{Atom.to_string(current_user_key)}_session_token") | ||
end | ||
|
||
defp assign_current_user_session_token(socket, user, config) do | ||
assign(socket, session_token_assign_key(config), user) | ||
end | ||
|
||
defp get_current_user_session_token(socket, config) do | ||
Map.get(socket.assigns, session_token_assign_key(config)) | ||
end | ||
|
||
defp get_current_user(session_token, config) do | ||
case Pow.Store.CredentialsCache.get([backend: config[:cache_store_backend]], session_token) do | ||
:not_found -> nil | ||
{user, _inserted_at} -> user | ||
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
defmodule PixelForumWeb.Live.ProfileLive do | ||
use PixelForumWeb, :live_view | ||
|
||
use PixelForumWeb.Live.AuthHelper, otp_app: :pixel_forum | ||
|
||
@impl true | ||
def mount(_params, session, socket) do | ||
socket = assign_current_user!(socket, session) | ||
|
||
{:ok, socket} | ||
end | ||
|
||
@impl true | ||
def session_expired(socket) do | ||
{:noreply, | ||
socket | ||
|> put_flash(:info, "Session expired.") | ||
|> push_redirect(to: Routes.page_path(socket, :index))} | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<section> | ||
<h1>Profile</h1> | ||
|
||
<%= if @current_user do %> | ||
<strong>User email:</strong> <%= @current_user.email %> | ||
<% end %> | ||
</section> |
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
defmodule PixelForumWeb.PageControllerTest do | ||
use PixelForumWeb.ConnCase, async: true | ||
|
||
describe "keep_alive" do | ||
test "Returns an ok response", %{conn: conn} do | ||
conn = get(conn, Routes.page_path(conn, :keep_alive)) | ||
assert text_response(conn, 200) == "ok" | ||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
defmodule PixelForumWeb.Live.AuthHelperTest do | ||
use PixelForumWeb.ConnCase, async: true | ||
import Phoenix.LiveViewTest | ||
import PixelForumWeb.Test.AuthHelper | ||
|
||
# TODO: Ideally, we should use a LiveView made only for testing, instead of a | ||
# real page. | ||
|
||
describe "when not logged in" do | ||
test "redirects to the index", %{conn: conn} do | ||
index_path = Routes.page_path(conn, :index) | ||
|
||
assert {:error, {:redirect, %{to: ^index_path}}} = | ||
live(conn, Routes.profile_path(conn, :index)) | ||
end | ||
end | ||
|
||
describe "when logged in" do | ||
setup [:log_as_user] | ||
|
||
test "shows current user info", %{conn: authed_conn, user: user} do | ||
{:ok, page_live, disconnected_html} = | ||
live(authed_conn, Routes.profile_path(authed_conn, :index)) | ||
|
||
assert disconnected_html =~ user.email | ||
assert render(page_live) =~ user.email | ||
end | ||
|
||
test "properly redirects when session expires", %{conn: authed_conn, user: user} do | ||
{:ok, page_live, _disconnected_html} = | ||
live(authed_conn, Routes.profile_path(authed_conn, :index)) | ||
|
||
assert render(page_live) =~ user.email | ||
|
||
expire_user_session(authed_conn) | ||
assert_redirect(page_live, "/", 200) | ||
end | ||
end | ||
|
||
defp expire_user_session(conn) do | ||
token = Plug.Conn.get_session(conn, session_token_key() <> "_unsigned") | ||
cache_store_backend = Pow.Config.get([], :cache_store_backend, Pow.Store.Backend.EtsCache) | ||
Pow.Store.CredentialsCache.delete([backend: cache_store_backend], token) | ||
end | ||
|
||
defp session_token_key(), do: Pow.Plug.prepend_with_namespace([otp_app: :pixel_forum], "auth") | ||
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
Oops, something went wrong.