mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +01:00
176 lines
4.5 KiB
Elixir
176 lines
4.5 KiB
Elixir
defmodule PhilomenaWeb.PowInvalidatedSessionPlug do
|
|
@moduledoc """
|
|
This plug ensures that invalidated sessions can still be used for a short
|
|
amount of time.
|
|
|
|
This MAY introduce a slight timing attack vector, but in practice would be
|
|
unlikely as all tokens expires after 60 seconds.
|
|
|
|
## Example
|
|
|
|
plug MyAppWeb.PowInvalidatedSessionPlug, :pow_session
|
|
plug MyAppWeb.PowInvalidatedSessionPlug, :pow_persistent_session
|
|
plug Pow.Plug.Session, otp_app: :my_app
|
|
plug PowPersistentSession.Plug.Cookie
|
|
plug MyAppWeb.PowInvalidatedSessionPlug, :load
|
|
|
|
"""
|
|
alias Plug.Conn
|
|
alias Pow.{Config, Plug, Store.Backend.EtsCache}
|
|
|
|
@store_ttl :timer.minutes(1)
|
|
@otp_app :philomena
|
|
@session_key "#{@otp_app}_auth"
|
|
@session_signing_salt Atom.to_string(Pow.Plug.Session)
|
|
@persistent_cookie_key "#{@otp_app}_persistent_session"
|
|
@persistent_cookie_signing_salt Atom.to_string(PowPersistentSession.Plug.Cookie)
|
|
|
|
def init(:load), do: :load
|
|
|
|
def init(:pow_session) do
|
|
[
|
|
fetch_token: &__MODULE__.client_store_fetch_session/1,
|
|
namespace: :session
|
|
]
|
|
end
|
|
|
|
def init(:pow_persistent_session) do
|
|
[
|
|
fetch_token: &__MODULE__.client_store_fetch_persistent_cookie/1,
|
|
namespace: :persistent_session
|
|
]
|
|
end
|
|
|
|
def init({type, opts}) do
|
|
type
|
|
|> init()
|
|
|> Keyword.merge(opts)
|
|
end
|
|
|
|
def call(conn, :load) do
|
|
Enum.reduce(conn.private[:invalidated_session_opts], conn, fn opts, conn ->
|
|
maybe_load_from_cache(conn, Plug.current_user(conn), opts)
|
|
end)
|
|
end
|
|
|
|
def call(conn, opts) do
|
|
fetch_fn = Keyword.fetch!(opts, :fetch_token)
|
|
token = fetch_fn.(conn)
|
|
|
|
conn
|
|
|> put_opts_in_private(opts)
|
|
|> Conn.register_before_send(fn conn ->
|
|
maybe_put_cache(conn, Plug.current_user(conn), token, opts)
|
|
end)
|
|
end
|
|
|
|
defp maybe_load_from_cache(conn, nil, opts) do
|
|
fetch_fn = Keyword.fetch!(opts, :fetch_token)
|
|
|
|
case fetch_fn.(conn) do
|
|
nil -> conn
|
|
token -> load_from_cache(conn, token, opts)
|
|
end
|
|
end
|
|
|
|
defp maybe_load_from_cache(conn, _any, _opts), do: conn
|
|
|
|
defp put_opts_in_private(conn, opts) do
|
|
plug_opts = (conn.private[:invalidated_session_opts] || []) ++ [opts]
|
|
|
|
Conn.put_private(conn, :invalidated_session_opts, plug_opts)
|
|
end
|
|
|
|
defp maybe_put_cache(conn, nil, _old_token, _opts), do: conn
|
|
defp maybe_put_cache(conn, _user, nil, _opts), do: conn
|
|
|
|
defp maybe_put_cache(conn, user, old_token, opts) do
|
|
fetch_fn = Keyword.fetch!(opts, :fetch_token)
|
|
|
|
metadata =
|
|
conn.private
|
|
|> Map.get(:pow_session_metadata, [])
|
|
|> Keyword.take([:valid_totp_at])
|
|
|
|
case fetch_fn.(conn) do
|
|
^old_token -> conn
|
|
_token -> put_cache(conn, {user, metadata}, old_token, opts)
|
|
end
|
|
end
|
|
|
|
defp put_cache(conn, user, token, opts) do
|
|
{store, store_config} = invalidated_cache(conn, opts)
|
|
|
|
store.put(store_config, token, user)
|
|
|
|
conn
|
|
end
|
|
|
|
defp load_from_cache(conn, token, opts) do
|
|
config = Plug.fetch_config(conn)
|
|
{store, store_config} = invalidated_cache(conn, opts)
|
|
|
|
case store.get(store_config, token) do
|
|
:not_found ->
|
|
conn
|
|
|
|
{user, metadata} ->
|
|
metadata = Keyword.merge(metadata, conn.private[:pow_session_metadata] || [])
|
|
|
|
conn
|
|
|> Conn.put_private(:pow_session_metadata, metadata)
|
|
|> Plug.assign_current_user(user, config)
|
|
|
|
user ->
|
|
Plug.assign_current_user(conn, user, config)
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def client_store_fetch_session(conn) do
|
|
conn =
|
|
conn
|
|
|> Plug.put_config(otp_app: @otp_app)
|
|
|> Conn.fetch_session()
|
|
|
|
with session_id when is_binary(session_id) <- Conn.get_session(conn, @session_key),
|
|
{:ok, session_id} <- Plug.verify_token(conn, @session_signing_salt, session_id) do
|
|
session_id
|
|
else
|
|
_any -> nil
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def client_store_fetch_persistent_cookie(conn) do
|
|
conn =
|
|
conn
|
|
|> Plug.put_config(otp_app: @otp_app)
|
|
|> Conn.fetch_cookies()
|
|
|
|
with token when is_binary(token) <- conn.cookies[@persistent_cookie_key],
|
|
{:ok, token} <- Plug.verify_token(conn, @persistent_cookie_signing_salt, token) do
|
|
token
|
|
else
|
|
_any -> nil
|
|
end
|
|
end
|
|
|
|
defp invalidated_cache(conn, opts) do
|
|
store_config = store_config(opts)
|
|
config = Plug.fetch_config(conn)
|
|
store = Config.get(config, :cache_store_backend, EtsCache)
|
|
|
|
{store, store_config}
|
|
end
|
|
|
|
defp store_config(opts) do
|
|
namespace = Keyword.fetch!(opts, :namespace)
|
|
ttl = Keyword.get(opts, :ttl, @store_ttl)
|
|
|
|
[
|
|
ttl: ttl,
|
|
namespace: "invalidated_#{namespace}"
|
|
]
|
|
end
|
|
end
|