elixirphoenix-frameworkplug

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 ...

Solution

  • 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 ...