pow
pow copied to clipboard
Instructions for WebSocket usage (e.g. Phoenix Channels and LiveView)
It's not obvious how to deal with Pow sessions and WebSockets. There are a few caveats to using WebSockets since browsers don't enforce CORS. Also, Phoenix LiveView won't run subsequent requests through the endpoint (so @current_user
is not available).
Some details on WebSocket security: https://devcenter.heroku.com/articles/websocket-security https://gist.github.com/subudeepak/9897212
Support for pulling session data in WebSockets was added to Phoenix in 1.4.7:
socket "/socket", AppWeb.UserSocket,
websocket: [
connect_info: [:peer_data, :x_headers, :uri, session: [store: :cookie]]
]
A few questions I want to answer are:
- Should the session be fetched for requests after initial handshake?
- If so, should the session be renewed after timeout in the socket? This would require the reply to update the session cookie.
- If not, should the socket be signed somehow, e.g. like a signed url? Not sure if this even makes sense.
- What should happen if the session expires while a socket is open (e.g. someone logs out). Should it be aware, and close the socket (if possible)?
I haven't worked much with WebSockets, so I'll have to read up on this and experiment. I will see if I can find some best practices when it comes to sessions and WebSockets. Any comments are welcome 😄
Here's a few links that may be of interest:
https://www.owasp.org/index.php/Testing_WebSockets_(OTG-CLIENT-010) https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#websockets https://spring.io/projects/spring-session https://github.com/spring-projects/spring-session https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html https://abhirockzz.wordpress.com/2017/06/03/accessing-http-session-in-websocket-endpoint/ https://abhirockzz.wordpress.com/2017/06/03/accessing-http-session-in-websocket-endpoint/
Could you rephrase the first question? Do you mean: "How should the session id be handled for requests after handshake?" ?
Rephrased, thanks!
From the elixir forum @LostKobrakai have used this:
https://gist.github.com/LostKobrakai/b51204a8de7ff463ee40bb6a3f6905b1
I would refactor it so:
- Rely on Pow for config e.g.
session_key
,cache_backend
,:session_store
, etc - Use the same naming convention as in Pow (e.g.
:current_user
) - Use
Process.send_after/3
instead of:timer.send_interval/3
(no need to cancel then) - Keep as little logic as possible in macros
Something like this:
defmodule LendingWeb.AuthHelper do
@moduledoc """
Handle pow user in LiveView.
Will assign the current user and periodically check that the session is still
active. `session_expired/1` will be called when session expires.
Configuration options:
* `:otp_app` - the app name
* `:interval` - how often the session has to be checked, defaults 60s
defmodule LendingWeb.SomeViewLive do
use PhoenixLiveView
use LendingWeb.AuthHelper, otp_app: lending
def mount(session, socket) do
socket = mount_user(socket, session)
# ...
end
def session_expired(socket) do
# handle session expiration
{:noreply, socket}
end
end
"""
require Logger
import Phoenix.Socket, only: [assign: 3]
defmacro __using__(opts) do
config = [otp_app: opts[:otp_app]]
session_key = Pow.Plug.prepend_with_namespace(config, "auth")
interval = Keyword.get(opts, :interval, :timer.seconds(60))
config = %{
session_key: session_key,
interval: interval,
module: __MODULE__
}
quote do
@config unquote(Macro.escape(config))
def mount_user(socket, session), do: unquote(__MODULE__).mount_user(socket, self(), session, @config)
def handle_info(:pow_auth_ttl, socket), do: unquote(__MODULE__).handle_auth_ttl(socket, self(), @config)
end
end
@spec mount_user(Phoenix.Socket.t(), pid(), map(), map()) :: Phoenix.Socket.t()
def mount_user(socket, pid, session, %{session_key: session_key} = config) do
user = Map.fetch!(session, session_key)
assign_key = Pow.Config.get(config, :current_user_assigns_key, :current_user)
socket
|> assign_current_user(user, config)
|> init_auth_check(pid, config)
end
defp init_auth_check(socket, pid, config) do
case Phoenix.LiveView.connected?(socket) do
true ->
handle_auth_ttl(socket, pid, config)
false ->
socket
end
end
@spec handle_auth_ttl(Phoenix.Socket.t(), pid(), map()) :: {:noreply, Phoenix.Socket.t()}
def handle_auth_ttl(socket, pid, %{interval: interval, module: module} = config) do
case pow_session_active?(config) do
true ->
Logger.info("[#{__MODULE__}] User session still active")
Process.send_after(pid, :pow_auth_ttl, interval)
{:noreply, socket}
false ->
Logger.info("[#{__MODULE__}] User session no longer active")
socket
|> assign_current_user(nil, config)
|> module.session_expired()
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 pow_session_active?(config) do
{store, store_config} = store(config)
store_config
|> store.get(key)
|> case do
:not_found -> false
{_user, _inserted_at} -> true
end
end
defp store(config) do
case Pow.Config.get(config, :session_store, default_store(config)) do
{store, store_config} -> {store, store_config}
store -> {store, []}
end
end
defp default_store(config) do
backend = Pow.Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)
{Pow.Store.CredentialsCache, [backend: backend]}
end
end
There's still an issue with sessions expiring after 30 minutes. The above doesn't keep sessions alive after that. The session id will also be rotated every 15 minutes. It's triggered if the user is visiting other pages while the socket is open.
The session could have a fingerprint, and that fingerprint can be used to look up the session info no matter if it has been rotated or not. This would make it possible to keep the socket open even after the session has been rotated.
If the cookie somehow can be updated in the session, then we can also prevent expiration after 30 min (since we'll then rotate within the socket).
I tried the example in a real project and after a few changes it works fine! Here is my final version:
defmodule LendingWeb.AuthHelper do
@moduledoc """
Handle pow user in LiveView.
Will assign the current user and periodically check that the session is still
active. `session_expired/1` will be called when session expires.
Configuration options:
* `:otp_app` - the app name
* `:interval` - how often the session has to be checked, defaults 60s
defmodule LendingWeb.SomeViewLive do
use PhoenixLiveView
use LendingWeb.AuthHelper, otp_app: lending
def mount(session, socket) do
socket = mount_user(socket, session)
# ...
end
def session_expired(socket) do
# handle session expiration
{:noreply, socket}
end
end
"""
require Logger
# `Phoenix.Socket.assign` doesn't accept `LiveView.Socket` as its
# first argument, so we have to use `Phoenix.LiveView.assign` to
# work with sockets from LiveView.
- import Phoenix.Socket, only: [assign: 3]
+ import Phoenix.LiveView, only: [assign: 3]
defmacro __using__(opts) do
config = [otp_app: opts[:otp_app]]
session_key = Pow.Plug.prepend_with_namespace(config, "auth")
interval = Keyword.get(opts, :interval, :timer.seconds(60))
# `config` is going to be passed to `Pow.Config.get` in several
# places, which uses `Keyword.get` under the hood, which expects
# the first argument to be a list, not a structure. So I changed
# the config to a list with keywords
- config = %{
+ config = [
session_key: session_key,
interval: interval,
# I also moved module from here to the `quote` block, because as
# I understood it's supposed to point at the `*Live` module which
# is going to use `AuthHelper`, because `AuthHelper` attempts to
# call `module.session_expired(socket)` when the session expires,
# but outside of the `quote` block `__MODULE__` points at the helper
# itself.
- module: __MODULE__,
]
quote do
# This is where I moved the `module` assignment
- @config unquote(Macro.escape(config))
+ @config unquote(Macro.escape(config)) ++ [module: __MODULE__]
def mount_user(socket, session), do: unquote(__MODULE__).mount_user(socket, self(), session, @config)
def handle_info(:pow_auth_ttl, socket), do: unquote(__MODULE__).handle_auth_ttl(socket, self(), @config)
end
end
@spec mount_user(Phoenix.Socket.t(), pid(), map(), map()) :: Phoenix.Socket.t()
# Since `config` is a list with keywords now, I suppose we can't pattern
# match it like this, and should work with as with a list
- def mount_user(socket, pid, session, %{session_key: session_key} = config) do
- user = Map.fetch!(session, session_key)
+ def mount_user(socket, pid, session, config) do
+ user = Map.fetch!(session, config[:session_key])
assign_key = Pow.Config.get(config, :current_user_assigns_key, :current_user)
# That's kinda tricky, but the point is that `mount_user` is expected
# to return socket, but it's possible that `init_auth_check` returns
# `{:noreply, socket}`, because it uses `handle_auth_ttl` under the
# hook, which, in turn, is supposed to return the tuple, because it
# can also be called from `handle_info(:pow_auth_ttl)`. So, instead
# of calling `handle_auth_ttl` from `mount_user` I decided to just
# send the `:pow_auth_ttl` message immediately, and thus guarantee
# that `mount_user` always returns a socket, and at the same time we
# still do an initial check.
- socket
- |> assign_current_user(user, config)
- |> init_auth_check(pid, config)
+ socket = socket |> assign_current_user(user, config)
+ init_auth_check(socket, pid, config)
+ socket
end
defp init_auth_check(socket, pid, config) do
# That's how I changed `init_auth_check` from calling `handle_auth_ttl`
# directly, to sending a message and thus call it indirectly. Also, I'm
# not quite familiar with Elixir, so there is probably a better way to
# send a message immediately instead of using `send_after` with 0 interval
- case Phoenix.LiveView.connected?(socket) do
- true ->
- handle_auth_ttl(socket, pid, config)
-
- false ->
- socket
- end
+ if Phoenix.LiveView.connected?(socket) do
+ Process.send_after(pid, :pow_auth_ttl, 0)
+ en
end
@spec handle_auth_ttl(Phoenix.Socket.t(), pid(), map()) :: {:noreply, Phoenix.Socket.t()}
# This first thing here is to work with `config` as with list
- def handle_auth_ttl(socket, pid, %{interval: interval, module: module} = config) do
+ def handle_auth_ttl(socket, pid, config) do
+ interval = Pow.Config.get(config, :interval)
+ module = Pow.Config.get(config, :module)
# And the second is to pass socket into the helper (you'll see why)
- case pow_session_active?(config) do
+ case pow_session_active?(socket, config) do
true ->
Logger.info("[#{__MODULE__}] User session still active")
Process.send_after(pid, :pow_auth_ttl, interval)
{:noreply, socket}
false ->
Logger.info("[#{__MODULE__}] User session no longer active")
socket
|> assign_current_user(nil, config)
|> module.session_expired()
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
# A small helper to extract user from socket, similarly to the
# `assign_current_user` above, which puts user to the socket.
+ defp get_current_user(socket, config) do
+ assign_key = Pow.Config.get(config, :current_user_assigns_key, :current_user)
+
+ socket.assigns |> Map.get(assign_key)
+ end
# Accepts `socket` now
- defp pow_session_active?(config) do
+ defp pow_session_active?(socket, config) do
{store, store_config} = store(config)
store_config
# And this is why we need the socket, because in the original
# example `key` wasn't defined, but supposed to be the auth ID
# extracted from session and put to the socket in `mount_user`.
# So here we need to extract that auth ID and run the check
# against it.
- |> store.get(key)
+ |> store.get(get_current_user(socket, config))
|> case do
:not_found -> false
{_user, _inserted_at} -> true
end
end
defp store(config) do
# And two small changes, because `Config` isn't aliased in the example,
# and wee need to use the full name of the module.
- case Config.get(config, :session_store, default_store(config)) do
+ case Pow.Config.get(config, :session_store, default_store(config)) do
{store, store_config} -> {store, store_config}
store -> {store, []}
end
end
defp default_store(config) do
- backend = Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)
+ backend = Pow.Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)
{Pow.Store.CredentialsCache, [backend: backend]}
end
end
Thanks @anatoliyarkhipov! I'm preparing the v1.0.14
release now that #287 has been merged in. After that I'll go through this to see how it can utilize the session fingerprint instead.
Thanks @danschultzer!
Also, I encountered a pitfall - between the moment of first rendering and the moment when the live view has mounted, there is a time period when we don't have a user. And it can lead to glitches in parts of UI that conditionally depend on user. For example, one might want to render an "Edit" button only when user is logged in. In this case the button will not be rendered during the first render, but will appear immediately after the live view is mounted. And this is a noticeable delay.
But I think it's not related specifically to Pow, but to LiveView in general instead, because the same UI glitch can be encountered for any variable that exists only after mounting and doesn't on the first render.
UPD: I was wrong and the UI glitch happened not because of LiveView nature, but because I changed the example to assign user to the socket asynchronously, via sending message, instead of doing it directly in mount_user
.
@anatoliyarkhipov Just having a play with the above code. Looks like you're assigning the current_user to just be the session key rather than the user itself? Is that a mistake? I'd have expected the value of 'current_user' to be the actual user
def mount_user(socket, pid, session, config) do
user = Map.fetch!(session, config[:session_key])
# That's kinda tricky, but the point is that `mount_user` is expected
# to return socket, but it's possible that `init_auth_check` returns
# `{:noreply, socket}`, because it uses `handle_auth_ttl` under the
# hood, which, in turn, is supposed to return the tuple, because it
# can also be called from `handle_info(:pow_auth_ttl)`. So, instead
# of calling `handle_auth_ttl` from `mount_user` I decided to just
# send the `:pow_auth_ttl` message immediately, and thus guarantee
# that `mount_user` always returns a socket, and at the same time we
# still do an initial check.
socket = socket |> assign_current_user_session_key(user, config)
init_auth_check(socket, pid, config)
socket
end
@morgz Yep, your statement is correct, in the example I assigned the session key instead of the whole user. Technically, I wouldn't call it a mistake, since I just adopted the original example 😀, but practically having only the key wasn't convenient for me, so later in my app I changed the code to assign the key at current_user_key
and the whole user at current_user
.
@anatoliyarkhipov I found your example really helpful - so thanks. I've adapted parts of it to assign the current_user to the socket. I'm new to Elixir so I'm sure this code can be improved upon (and I welcome your feedback!) I decided to avoid calling the handle_info with a delay of 0 in favour of directly calling to get the current_user into the socket on mount.
defmodule WildeWeb.Live.AuthHelper do
@moduledoc """
Handle pow user in LiveView.
Will assign the current user and periodically check that the session is still
active. `session_expired/1` will be called when session expires.
Configuration options:
* `:otp_app` - the app name
* `:interval` - how often the session has to be checked, defaults 60s
defmodule LendingWeb.SomeViewLive do
use PhoenixLiveView
use LendingWeb.Live.AuthHelper, otp_app: :otp_app
def mount(session, socket) do
socket = mount_user(socket, session)
# ...
end
def session_expired(socket) do
# handle session expiration
{:noreply, socket}
end
end
"""
require Logger
import Phoenix.LiveView, only: [assign: 3]
defmacro __using__(opts) do
config = [otp_app: opts[:otp_app]]
session_key = Pow.Plug.prepend_with_namespace(config, "auth") |> String.to_existing_atom
interval = Keyword.get(opts, :interval, :timer.seconds(60))
config = [
session_key: session_key,
interval: interval
]
quote do
@config unquote(Macro.escape(config)) ++ [module: __MODULE__]
def mount_user(socket, session), do: unquote(__MODULE__).mount_user(socket, self(), session, @config)
def handle_info(:pow_auth_ttl, socket), do: unquote(__MODULE__).handle_auth_ttl(socket, self(), @config)
end
end
@spec mount_user(Phoenix.Socket.t(), pid(), map(), map()) :: Phoenix.Socket.t()
def mount_user(socket, pid, session, config) do
user_session_key = Map.fetch!(session, config[:session_key])
user = current_user(user_session_key, config)
# Start our interval check to see if the current_user is still value
init_auth_check(socket, pid, config)
socket
# Assigns the session key from the session to the assigns of the socket so it is persisted.
|> assign_current_user_session_key(user_session_key, config)
# Assigns the user into the :current_user_assigns_key defineed by POW. Default :current_user
|> assign_current_user(user, config)
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
@spec handle_auth_ttl(Phoenix.Socket.t(), pid(), map()) :: {:noreply, Phoenix.Socket.t()}
def handle_auth_ttl(socket, pid, config) do
interval = Pow.Config.get(config, :interval)
module = Pow.Config.get(config, :module)
session_key = get_current_user_session_key(socket, config)
case current_user(session_key, config) do
nil -> Logger.info("[#{__MODULE__}] User session no longer active")
socket
|> assign_current_user_session_key(nil, config)
|> assign_current_user(nil, config)
|> module.session_expired()
_user -> Logger.info("[#{__MODULE__}] User session still active")
Process.send_after(pid, :pow_auth_ttl, interval)
{:noreply, socket}
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 assign_current_user_session_key(socket, user, config) do
assign_key = config[:session_key]
assign(socket, assign_key, user)
end
# Helper to extract the session_key from socket
defp get_current_user_session_key(socket, config) do
assign_key = config[:session_key]
socket.assigns |> Map.get(assign_key)
end
# Helper to extract the current_user from store
defp current_user(session_key, config) do
{store, store_config} = store(config)
case store_config |> store.get(session_key) do
:not_found -> nil
{user, _inserted_at} -> user
end
end
defp store(config) do
case Pow.Config.get(config, :session_store, default_store(config)) do
{store, store_config} -> {store, store_config}
store -> {store, []}
end
end
defp default_store(config) do
backend = Pow.Config.get(config, :cache_store_backend, Pow.Store.Backend.MnesiaCache)
{Pow.Store.CredentialsCache, [backend: backend]}
end
end
@danschultzer Thank you for building Pow! Would you by any chance already have some guidance on how to use the session fingerprint (either as part of the above code, or in general)?
@dbi1 Thanks!
No, I haven't had time to dive into this, so I only have some light thoughts on it.
- Mount with current user struct and session fingerprint.
Pow.Store.CredentialsCache.get/2
now returns{user, metadata}
where metadata is a keyword list that in most cases as a:fingerprint
key. - Use the current user struct to check if the session is still available by fetching all sessions with
Pow.Store.CredentialsCache.sessions/2
and search for a session that has the fingerprint. - Maybe update TTL by using
Pow.Store.CredentialsCache.put/3
, storing the same session id, and{user, metadata}
as was just fetched in 2. This could potentially open up for session attacks, but unless there's a way to set cookies through live view, I don't see any other way of keeping the session alive.
@danschultzer Would be session kept alive if we periodically ping server from JS, making some AJAX requests? I mean, not a request through WebSocket, but a regular AJAX request.
Would be session kept alive if we periodically ping server from JS, making some AJAX requests?
Yeah it would since that would trigger renewal in Pow.Plug.Session
when the endpoint is called. Then step 3 in the above is not necessary, all we need to know is the fingerprint and user to check if the session hasn't expired. It's the best solution I can think of.
Honestly, I'm new to Elixir, and I'm constantly baffled by two questions: "what is what?" and "where do I get that what?". And this time is not an exception 😬.
- What is
config
in Pow.Store.CredentialsCache.get/2? - Where do I get it (assuming we're working in the example above)?
Okay, I figured it out and here is my final version:
defmodule MyAppWeb.Live.AuthHelper do
require Logger
import Phoenix.LiveView, only: [assign: 3]
defmacro __using__(opts) do
config = [otp_app: opts[:otp_app]]
session_id_key = Pow.Plug.prepend_with_namespace(config, "auth")
auth_check_interval = Keyword.get(opts, :auth_check_interval, :timer.seconds(1))
config = [
session_id_key: session_id_key,
auth_check_interval: auth_check_interval,
]
quote do
@config unquote(Macro.escape(config)) ++ [
live_view_module: __MODULE__,
]
def mount_user(socket, session),
do: unquote(__MODULE__).mount_user(socket, self(), session, @config)
def handle_info(:pow_auth_ttl, socket),
do: unquote(__MODULE__).handle_auth_ttl(socket, self(), @config)
end
end
def mount_user(socket, pid, session, config) do
session_id = Map.fetch!(session, config[:session_id_key])
case credentials_by_session_id(session_id) do
{user, meta} ->
socket = socket |> assign(:credentials, {user, meta})
if Phoenix.LiveView.connected?(socket) do
init_auth_check(pid)
end
socket
everything_else ->
socket
end
end
defp init_auth_check(pid) do
Process.send_after(pid, :pow_auth_ttl, 0)
end
def handle_auth_ttl(socket, pid, config) do
{user, meta} = socket.assigns[:credentials]
live_view_module = Pow.Config.get(config, :live_view_module)
auth_check_interval = Pow.Config.get(config, :auth_check_interval)
case session_id_by_credentials(socket.assigns[:credentials]) do
nil ->
Logger.info("[#{__MODULE__}] User session no longer active")
{:noreply, socket |> assign(:credentials, nil)}
_session_id ->
Logger.info("[#{__MODULE__}] User session still active")
Process.send_after(pid, :pow_auth_ttl, auth_check_interval)
{:noreply, socket}
end
end
defp session_id_by_credentials(nil), do: nil
defp session_id_by_credentials({user, meta}) do
all_user_session_ids = Pow.Store.CredentialsCache.sessions(
[backend: Pow.Store.Backend.EtsCache],
user
)
all_user_session_ids |> Enum.find(fn session_id ->
{_, session_meta} = credentials_by_session_id(session_id)
session_meta[:fingerprint] == meta[:fingerprint]
end)
end
defp credentials_by_session_id(session_id) do
Pow.Store.CredentialsCache.get(
[backend: Pow.Store.Backend.EtsCache],
session_id
)
end
end
It works fine, but it feels wrong that I access CredentialsCache
directly and provide the config with EtsCache
backend when I already have it defined for Pow.Plug.Session
in the endpoint.ex
:
plug Pow.Plug.Session,
otp_app: :my_app,
session_store: {Pow.Store.CredentialsCache,
ttl: :timer.minutes(30),
namespace: "credentials"},
session_ttl_renewal: :timer.minutes(15)
I mean, what if I change Pow.Store.CredentialsCache
to something else in this config? I'll have to remember to change it the live AuthHelper
as well. right? Is there any way how I can access config passed to Pow.Plug.Session
from AuthHelper
? 🤔
@anatoliyarkhipov just a heads up, I’m traveling and don’t have any laptop with me. I’m waiting till I’m back as I would like to refactor the code, and be able to properly answer your questions. I’ll be back Tuesday.
With the release of LiveView 0.5.1 today, there have been a number of improvements regarding sessions in the socket. I'm not sure if that addresses any of the issues here, but thought I'd mention it since I'm needing to implement this as well.
https://github.com/phoenixframework/phoenix_live_view/blob/master/CHANGELOG.md#050-2020-01-15
Hey everyone - Just wanted to check in on this issue. Has anyone had any more thought on keeping the session alive? I'm going to be working on this in about a weeks time
@morgz I settled with the latest version I posted in the thread. With an interval ping from JS.
@morgz I settled with the latest version I posted in the thread. With an interval ping from JS.
Thanks - It would be helpful to me if you could you share your implementation of the interval ping?
That's a direct copy-n-paste from the codebase, including comments (which might be wrong, if I misinterpreted something at the moment I implemented that).
That function I call in the main JS file when it's loaded:
import {wait} from "./utils"
/**
* If user doesn't request server long enough (few minutes),
* his session expires and he has to re-login again. If user
* manages to request server before the time has expired, his
* cookie is updated and the timer is reset.
*
* The problem is that almost the whole website uses LiveView,
* even for navigation, which means that most of the requests
* go through WebSockets, where you can't update cookies, and
* so the session inevitably expires, even if user is actively
* using the website. More of that - it might expire during an
* editing of a project, and user will be redirected, loosing
* all its progress. What a shame!
*
* To work this around, we periodically ping the server via a
* regular AJAX requests, which is noticed by the auth system
* which, in turn, resets the cookie timer.
*/
export function keepAlive() {
fetch('/keep-alive')
.then(wait(60 * 1000 /*ms*/))
.then(keepAlive)
}
The wait:
export function wait(ms) {
return () => new Promise(resolve => {
setTimeout(resolve, ms)
})
}
The /keep-alive
endpoint does nothing.
Excellent. Thanks @anatoliyarkhipov ✌️ I'll give it a go and report back
where do you call authhelper? is it just a use statement in the liveview?
where do you call authhelper? is it just a use statement in the liveview?
Exactly, something like this:
defmodule AppWeb.Live.Index do
use AppWeb.Live.AuthHelper, otp_app: :app_name
def mount(%{"id" => id} = params, session, socket) do
socket = maybe_mount_user(socket, session)
end
end
Ok, I mount user, check credentials in session , but what should i do with logout event in the live view?
something like this Pow.Store.CredentialsCache.delete([backend: Pow.Store.Backend.MnesiaCache], session_id)
?
Note that by default since a few versions ago that you can broadcast "disconnect" to the LV socket id to invalidate/kill any active user connections on logout, as long as you add the :live_socket_id
to the session on login:
https://github.com/phoenixframework/phoenix_live_view/blob/b49e828a15a0121d5cced8691089cadc122eb293/lib/phoenix_live_view.ex#L991-L1004
Maybe I'm overlooking something but for my use-case just adding some code to UserSocket
works fine.
defmodule BlaBlaWeb.UserSocket do
use Phoenix.Socket
def connect(%{"authToken" => token}, socket, _connect_info) do
result = Pow.Store.CredentialsCache.get([backend: Pow.Store.Backend.EtsCache], token)
case result do
:not_found -> :error
{user, _metadata} -> {:ok, assign(socket, :current_user, user)}
end
end
# we need an authToken
def connect(_params, _socket, _connect_info), do: :error
# ....
end
Use-case: mixed API and phoenix channels
I have a React app hosted on a different domain. It uses a mix of normal HTTP JSON API requests and WebSocket updates.
I made a API endpoint for token based auth using pretty much a copy and paste version of https://hexdocs.pm/pow/api.html
-
The user logs in user via a webform or automatically using the
renew_token
(stored in localStorage). -
Then after succes the websockets tries to connect, the user/session is already known to Pow. Also the token is on the client so I just send that along with the
connect()
. After that it's just a matter of looking up the user and assigning it to the socket.
client code:
const socket = new Socket(`${baseUrl.replace('http', 'ws')}/socket`, { params: { authToken: auth.token } });
Has anyone gotten this approach to work with the latest changes to pow (and live view)? I can no longer fetch the user from the session. This is now failing with :not_found
in my auth helper:
Pow.Store.CredentialsCache.get([backend: Pow.Store.Backend.EtsCache], session_id)
@trestrantham the latest version of Pow (v1.0.19) signs and verifies the token, so you'll have to decode it before looking up:
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
def connect(%{"authToken" => token}, socket, _connect_info) do
case get_credentials(socket, token, otp_app: :my_app) do
nil -> :error
user -> {:ok, assign(socket, :current_user, user)}
end
end
# we need an authToken
def connect(_params, _socket, _connect_info), do: :error
defp get_credentials(socket, signed_token, config) do
conn = %Plug.Conn{secret_key_base: socket.endpoint.config(:secret_key_base)}
store_config = [backend: Pow.Store.Backend.EtsCache]
salt = Atom.to_string(Pow.Plug.Session)
with {:ok, token} <- Pow.Plug.verify_token(conn, salt, signed_token, config),
{_user, metadata} <- Pow.Store.CredentialsCache.get(store_config, token) do
user
else
_any -> nil
end
end
# ....
end
Also now with LiveView built into Phoenix 1.5, it's time for official support in Pow. I'll get back to this ASAP. I think I'll add a Pow.Phoenix.Socket
module with helpers.
@danschultzer Adding official helpers would be amazing!
I was originally referring to the solution here https://github.com/danschultzer/pow/issues/271#issuecomment-562953282 and was able to modify it to work with the example you provided. Thanks!
I did also try the UserSocket
approach but could not get it working. This is for a LiveView
so I tried to copy and modify Phoenix.LiveView.Socket
to add the user lookup but couldn't get it to work.
Also now with LiveView built into Phoenix 1.5, it's time for official support in Pow. I'll get back to this ASAP. I think I'll add a Pow.Phoenix.Socket module with helpers
Made my day! 👍 God speed....