Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elixir / Phoenix: How to implement session timeout / expiration

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 ...
like image 220
Topher Hunt Avatar asked Jan 29 '17 18:01

Topher Hunt


1 Answers

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 ...
like image 167
Topher Hunt Avatar answered Sep 28 '22 18:09

Topher Hunt