I'm working on a vanilla Elixir / Phoenix application and followed the general steps in the Programming Phoenix book to implement a basic sign in & sign out system (see snippets below). However I see no advice in the book or online about how to set up cookie-based Plug sessions to expire after a certain amount of time. What are some approaches to session timeout in Phoenix apps?
Here's some relevant snippets of my bare-bones auth system:
In endpoint.ex
, the app is configured to use a read-only cookie-based session:
plug Plug.Session,
store: :cookie,
key: "_zb_key",
signing_salt: "RANDOM HEX"
I wrote a plug auth.ex
which (among other things) can log in an authenticated user, and can set current_user
based on the session user_id
found in subsequent requests:
def login!(conn, user) do
conn
|> assign(:current_user, user)
|> put_session(:user_id, user.id)
|> configure_session(renew: true)
end
# ... more ...
def load_current_user(conn, _opts) do
cond do
conn.assigns[:current_user] ->
conn # If :current_user was already set, honor it
user_id = get_session(conn, :user_id) ->
user = Zb.Repo.get!(Zb.User, user_id)
assign(conn, :current_user, user)
true ->
conn # No user_id was found; make no changes
end
end
# ... more ...
I first looked for cookie expiration options in the Plug library, then realized that an easier (and more secure) approach is to simply set an expiration datetime in the session along with the user_id. The session is tamper-proof, so when I receive each request, I can compare the datetime to now; if the session hasn't yet expired, I set current_user
as normal. Otherwise I call logout!
to delete the expired session.
An implementation would look something like this (requires the Timex library):
# Assign current_user to the conn, if a user is logged in
def load_current_user(conn, _opts) do
cond do
no_login_session?(conn) ->
conn # No user_id was found; make no changes
current_user_already_set?(conn) ->
conn
session_expired?(conn) ->
logout!(conn)
user = load_user_from_session(conn) ->
conn
|> put_session(:expires_at, new_expiration_datetime_string)
|> assign(:current_user, user)
end
end
defp session_expired?(conn) do
expires_at = get_session(conn, :expires_at) |> Timex.parse!("{ISO:Extended}")
Timex.after?(Timex.now, expires_at)
end
# ... more ...
# Start a logged-in session for an (already authenticated) user
def login!(conn, user) do
conn
|> assign(:current_user, user)
|> put_session(:user_id, user.id)
|> put_session(:expires_at, new_expiration_datetime_string)
|> configure_session(renew: true)
end
defp new_expiration_datetime_string do
Timex.now |> Timex.shift(hours: +2) |> Timex.format("{ISO:Extended}")
end
# ... more ...