Current section

Files

Jump to
pow lib extensions persistent_session plug cookie.ex
Raw

lib/extensions/persistent_session/plug/cookie.ex

defmodule PowPersistentSession.Plug.Cookie do
@moduledoc """
This plug will handle persistent user sessions with cookies.
By default, the cookie will expire after 30 days. The cookie expiration will
be renewed on every request. The token in the cookie can only be used once to
create a session.
If an assigned private `:pow_session_metadata` key exists in the conn with a
keyword list containing a `:fingerprint` key, that fingerprint value will be
set along with the user id as the persistent session value as
`{user_id, session_fingerprint: fingerprint}`.
## Example
defmodule MyAppWeb.Endpoint do
# ...
plug Pow.Plug.Session, otp_app: :my_app
plug PowPersistentSession.Plug.Cookie
#...
end
## Configuration options
* `:persistent_session_store` - see `PowPersistentSession.Plug.Base`
* `:cache_store_backend` - see `PowPersistentSession.Plug.Base`
* `:persistent_session_cookie_key` - session key name. This defaults to
"persistent_session_cookie". If `:otp_app` is used it'll automatically
prepend the key with the `:otp_app` value.
* `:persistent_session_ttl` - used for both backend store and max age for
cookie. See `PowPersistentSession.Plug.Base` for more.
"""
use PowPersistentSession.Plug.Base
alias Plug.Conn
alias Pow.{Config, Plug, UUID}
@cookie_key "persistent_session_cookie"
@doc """
Sets a persistent session cookie with an auto generated token.
The token is set as a key in the persistent session cache with the id fetched
from the struct.
If an assigned private `:pow_session_metadata` key exists in the conn with a
keyword list containing a `:fingerprint` value, then that value will be set
as the `:session_fingerprint` in the metadata. The value will look like:
`{user_id, session_fingerprint: fingerprint}`
The unique cookie id will be prepended by the `:otp_app` configuration
value, if present.
"""
@spec create(Conn.t(), map(), Config.t()) :: Conn.t()
def create(conn, user, config) do
{store, store_config} = store(config)
cookie_key = cookie_key(config)
key = cookie_id(config)
value = persistent_session_value(conn, user)
opts = session_opts(config)
store.put(store_config, key, value)
Conn.put_resp_cookie(conn, cookie_key, key, opts)
end
defp persistent_session_value(conn, user) do
clauses = user_to_get_by_clauses(user)
conn.private
|> Map.get(:pow_session_metadata, [])
|> Keyword.get(:fingerprint)
|> case do
nil -> clauses
fingerprint -> {clauses, session_fingerprint: fingerprint}
end
end
defp user_to_get_by_clauses(%{id: id}), do: [id: id]
@doc """
Expires the persistent session cookie.
If a persistent session cookie exists it'll be expired, and the token in
the persistent session cache will be deleted.
"""
@spec delete(Conn.t(), Config.t()) :: Conn.t()
def delete(conn, config) do
cookie_key = cookie_key(config)
case conn.req_cookies[cookie_key] do
nil -> conn
key_id -> do_delete(conn, cookie_key, key_id, config)
end
end
defp do_delete(conn, cookie_key, key_id, config) do
{store, store_config} = store(config)
value = ""
opts = [max_age: -1, path: "/"]
store.delete(store_config, key_id)
Conn.put_resp_cookie(conn, cookie_key, value, opts)
end
@doc """
Authenticates a user with the persistent session cookie.
If a persistent session cookie exists, it'll fetch the credentials from the
persistent session cache, and create a new session and persistent session
cookie. The old persistent session cookie and session cache credentials will
be removed.
If a `:session_fingerprint` is fetched from the persistent session metadata,
it'll be assigned to the private `:pow_session_metadata` key in the conn as
`:fingerprint`.
The cookie expiration will automatically be renewed on every request.
"""
@spec authenticate(Conn.t(), Config.t()) :: Conn.t()
def authenticate(conn, config) do
user = Plug.current_user(conn, config)
conn
|> Conn.fetch_cookies()
|> maybe_authenticate(user, config)
|> maybe_renew(config)
end
defp maybe_authenticate(conn, nil, config) do
cookie_key = cookie_key(config)
case conn.req_cookies[cookie_key] do
nil -> conn
key_id -> do_authenticate(conn, key_id, config)
end
end
defp maybe_authenticate(conn, _user, _config), do: conn
defp do_authenticate(conn, key_id, config) do
{store, store_config} = store(config)
res = store.get(store_config, key_id)
plug = Plug.get_plug(config)
conn = delete(conn, config)
case res do
:not_found -> conn
res -> fetch_and_auth_user(conn, res, plug, config)
end
end
defp fetch_and_auth_user(conn, {clauses, metadata}, plug, config) do
conn = update_session_metadata_with_fingerprint(conn, metadata)
clauses
|> filter_invalid!()
|> Pow.Operations.get_by(config)
|> case do
nil ->
conn
user ->
conn
|> create(user, config)
|> plug.do_create(user, config)
end
end
defp fetch_and_auth_user(conn, user_id, plug, config),
do: fetch_and_auth_user(conn, {user_id, []}, plug, config)
defp filter_invalid!([id: _value] = clauses), do: clauses
defp filter_invalid!(clauses), do: raise "Invalid get_by clauses stored: #{inspect clauses}"
defp update_session_metadata_with_fingerprint(conn, metadata) do
case Keyword.get(metadata, :session_fingerprint) do
nil ->
conn
fingerprint ->
metadata =
conn.private
|> Map.get(:pow_session_metadata, [])
|> Keyword.put(:fingerprint, fingerprint)
Conn.put_private(conn, :pow_session_metadata, metadata)
end
end
defp maybe_renew(conn, config) do
cookie_key = cookie_key(config)
case conn.resp_cookies[cookie_key] do
nil -> renew(conn, cookie_key, config)
_any -> conn
end
end
defp renew(conn, cookie_key, config) do
opts = session_opts(config)
case conn.req_cookies[cookie_key] do
nil -> conn
value -> Conn.put_resp_cookie(conn, cookie_key, value, opts)
end
end
defp cookie_id(config) do
uuid = UUID.generate()
Plug.prepend_with_namespace(config, uuid)
end
defp cookie_key(config) do
Config.get(config, :persistent_session_cookie_key, default_cookie_key(config))
end
defp default_cookie_key(config) do
Plug.prepend_with_namespace(config, @cookie_key)
end
defp session_opts(config) do
[max_age: max_age(config), path: "/"]
end
defp max_age(config) do
# TODO: Remove by 1.1.0
case Config.get(config, :persistent_session_cookie_max_age) do
nil ->
config
|> PowPersistentSession.Plug.Base.ttl()
|> Integer.floor_div(1000)
max_age ->
IO.warn("use of `:persistent_session_cookie_max_age` config value in #{inspect unquote(__MODULE__)} is deprecated, please use `:persistent_session_ttl`")
max_age
end
end
end