Sigra uses database-backed session tokens (not stateful JWTs for sessions). A login creates a row in users_tokens, writes a reference to the Phoenix session cookie, and optionally sets a long-lived remember-me cookie. Logout deletes the row and clears the cookies. This guide covers the flow, remember-me, session renewal, and logout-everywhere.
What Sigra gives you
MyAppWeb.UserAuth.log_in_user/3— creates a session token, renews the session ID (defeats session fixation), sets the remember-me cookie when requested, and redirects.MyAppWeb.UserAuth.log_out_user/1— deletes the token from the database, renews the session, clears the remember-me cookie, and redirects.MyAppWeb.UserAuth.fetch_current_scope/2— plug that reads the session/remember-me cookie on every request and assigns:current_scopeto the conn.MyApp.Accounts.get_user_by_email_and_password/2— enumeration-safe lookup; returnsnilfor both wrong password and unknown email.MyApp.Accounts.delete_user_session_token/1— deletes a single session token.MyApp.Accounts.delete_all_user_session_tokens/1— the "log out everywhere" primitive.
Happy path
The generated SessionController.create/2 (or its LiveView equivalent) calls:
def create(conn, %{"user" => %{"email" => email, "password" => password} = params}) do
case Accounts.get_user_by_email_and_password(email, password) do
%User{} = user ->
conn
|> put_flash(:info, "Welcome back!")
|> UserAuth.log_in_user(user, params)
nil ->
conn
|> put_flash(:error, "Invalid email or password")
|> redirect(to: ~p"/users/log-in")
end
endlog_in_user/3 does four things:
- Generates a 32-byte random token, stores its SHA-256 hash in
users_tokenswithcontext: "session". - Calls
Plug.Conn.configure_session(renew: true)to rotate the Phoenix session ID. - Writes the raw token to the Phoenix session under
:user_token. - If
params["user"]["remember_me"] == "true", sets the signed remember-me cookie with the same token.
Remember-me
The remember-me cookie is signed (not encrypted) by Phoenix and contains the same token that's in the Phoenix session. Sigra reads it in fetch_current_scope/2 on the next visit:
defp ensure_user_token(conn) do
if token = get_session(conn, :user_token) do
{token, conn}
else
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
if token = conn.cookies[@remember_me_cookie] do
{token, put_session(conn, :user_token, token)}
else
{nil, conn}
end
end
endIf the Phoenix session cookie expires (browser close), the remember-me cookie rehydrates it. The session token itself is a DB row, so you can invalidate it server-side at any time.
Configuration
# config/config.exs
config :my_app, MyApp.Auth.Config,
session_ttl: 5_184_000 # 60 days in seconds (default)Remember-me cookie options — :max_age, :same_site, :http_only, :secure, :sign — live in lib/my_app_web/user_auth.ex. In production the :secure flag is automatically enabled and the :domain is read from Sigra.Config.cookie_domain at runtime (see Subdomain Authentication).
Session renewal
UserAuth.log_in_user/3 always calls renew_session/1 before writing the new token. This prevents session-fixation attacks where an attacker sets a known session ID on the victim, waits for them to authenticate, then uses that same ID.
You should also call renew_session/1 after any privilege change — for example, after confirming sudo mode or completing MFA:
conn
|> UserAuth.renew_session()
|> put_session(:user_token, new_token)Logout
The generator adds a DELETE /users/log-out route to the authenticated pipeline. It dispatches to:
def log_out_user(conn) do
user_token = get_session(conn, :user_token)
user_token && Accounts.delete_user_session_token(user_token)
conn
|> renew_session()
|> delete_resp_cookie(@remember_me_cookie)
|> redirect(to: ~p"/")
endThree things happen:
- The DB row is deleted. Any other browser still holding this same token is now broken on next visit.
- The Phoenix session ID is rotated.
- The remember-me cookie is cleared.
Log out everywhere
For a "log out of all devices" button (common on account security pages), call:
def delete_all_sessions(conn, _params) do
user = conn.assigns.current_scope.user
Accounts.delete_all_user_session_tokens(user)
conn
|> UserAuth.log_out_user()
|> put_flash(:info, "Logged out of all devices.")
enddelete_all_user_session_tokens/1 issues a single delete_all against users_tokens for that user, context "session". Every session — on every device — is invalidated.
Protecting routes
In lib/my_app_web/router.ex:
pipeline :require_authenticated_user do
plug :fetch_session
plug :fetch_current_scope
plug :require_authenticated_user
end
scope "/", MyAppWeb do
pipe_through [:browser, :require_authenticated_user]
live_session :authenticated,
on_mount: [{MyAppWeb.UserAuth, :require_authenticated}] do
live "/dashboard", DashboardLive
end
endrequire_authenticated_user/2 redirects unauthenticated requests to /users/log-in and stores the original path in the session so the user lands back where they started after login.
Testing
setup do
%{user: user, conn: conn} = Sigra.Testing.authenticated_fixture()
%{user: user, conn: conn}
end
test "shows dashboard when logged in", %{conn: conn} do
conn = get(conn, ~p"/dashboard")
assert html_response(conn, 200) =~ "Dashboard"
end
test "log-out clears the session", %{conn: conn, user: user} do
conn = delete(conn, ~p"/users/log-out")
assert redirected_to(conn) == ~p"/"
refute get_session(conn, :user_token)
endRelated
- Getting Started — the end-to-end walkthrough.
- Password Reset — password reset invalidates all sessions.
- Subdomain Authentication — sharing cookies across subdomains.
- MFA — adding a second factor to the login flow.
Sigra.Auth—create_session/4,delete_session/3,delete_all_sessions/3.