Auth
lib/auth_middleware.march — Session-based authentication for Bastion.
Provides the four core auth operations on typed conns:
load_current_user — soft check, never halts; reads user from session
require_auth — hard gate; redirects to /login when unauthenticated
log_in — session fixation rotation + set user ID
log_out — clear session (+ optional remember-me cookie cleanup)All functions operate on the conn-level session managed by Session.march. No database access is needed by the framework itself — the application injects a loader function (and optionally a remember_verifier) so that Bastion stays decoupled from the ORM.
The remember-me token is an opaque string generated by the application (e.g. via Accounts.create_remember_token). The framework stores it in a long-lived _remember_me cookie and delegates verification back to the app. The actual token creation and storage in user_tokens is wired up by forge gen.auth session.
Typical pipeline usage:
conn
|> Session.load(secret_key_base)
|> Auth.load_current_user(fn conn id -> MyApp.Accounts.get_user(conn, id) end)
|> MyApp.Router.route()In a handler requiring authentication:
with Ok(tc) <- Auth.require_auth(tc) do
render(tc, "dashboard.html", assigns)
else
Error(conn) -> conn
endLogin:
conn |> Auth.log_in(user.id)
conn |> Auth.log_in(user.id, remember_token: token)Logout:
conn |> Auth.log_out()Functions
Sugar for the common require_auth pattern.
Calls `handler` with the conn if authenticated; otherwise returns the
halted redirect conn directly.
Auth.authenticated(conn, fn conn ->
render(conn, "dashboard.html", assigns)
end)Get the current user from conn assigns.
Returns `Some(user)` if `load_current_user` was called and found a user,
or `None` if not authenticated. Callable at any point after the pipeline.
match Auth.current_user(conn) do
Some(u) -> "Hello, " ++ u.name
None -> "Guest"
endLoad the current user from the session into conn assigns.
Reads `current_user_id` from the session, converts it to an Int, and
calls `loader(conn, user_id)`. The result is stored in `current_user`
assigns as `Some(user)` on success or `None` if:
- no user ID is in the session
- the ID cannot be parsed as an Int
- the loader returns `Error(_)` (e.g. user was deleted)
This middleware is **soft** — it never halts. Use `require_auth/1` inside
route handlers to enforce authentication.
`loader` receives the unwrapped conn so it can access `TypedMiddleware.get_db`
for database-backed lookups.
conn |> Auth.load_current_user(fn conn id ->
MyApp.Accounts.get_user(conn, id)
end)Load the current user, falling back to a remember-me cookie when the session is empty.
Identical to `load_current_user/2` but also checks the `_remember_me`
cookie when there is no session user ID. If the cookie is present,
calls `remember_verifier(conn, token)` which should return
`Ok(user_id_int)` for a valid token or `Error(_)` for an expired/invalid
one. On success a fresh session is written (clear + set user ID — the
same rotation `log_in` performs), so subsequent requests authenticate via
the short-lived session cookie instead of re-verifying the long-lived
remember token on every request.
conn |> Auth.load_current_user_with_remember(
fn conn id -> MyApp.Accounts.get_user(conn, id) end,
fn conn token -> MyApp.Accounts.verify_remember_token(conn, token) end
)Log in a user by setting their ID in the session.
Clears the session first (session fixation protection) then sets
`current_user_id` to `user_id`. If `remember_token` is `Some(token)`,
also writes a persistent `_remember_me` cookie that lasts 60 days.
The `remember_token` should be generated and persisted to `user_tokens`
by the application before calling `log_in`:
let token = Accounts.create_remember_token(conn, user.id)
conn |> Auth.log_in(user.id, Some(token))
Pass `None` (or omit) for browser-session-only login:
conn |> Auth.log_in(user.id, None)Log out the current user by clearing the session.
Always clears the `_remember_me` cookie regardless of whether it was set.
If `token_deleter` is `Some(fn)`, calls `token_deleter(conn, token)` to
delete the token from the database before clearing the cookie.
Typical usage without remember-me:
conn |> Auth.log_out(None)
With remember-me cleanup (generated by `forge gen.auth session`):
conn |> Auth.log_out(Some(fn conn token ->
Accounts.delete_remember_token(conn, token)
end))Redirect to the stored return_to path after a successful login, or fall back to default_path if no return path was stored.
Call this at the end of a successful login handler:
conn
|> Auth.log_in(user.id)
|> Auth.redirect_after_login("/dashboard")Require an authenticated user. Call inside route handlers.
Returns `Ok(conn)` when `current_user` is `Some(user)`.
Returns `Error(conn)` with a halted 302 redirect to `/login` when not.
Before redirecting, stores the current request path in the session under
`return_to` so the login handler can redirect back after authentication.
with Ok(conn) <- Auth.require_auth(conn) do
render(conn, "dashboard.html", assigns)
else
Error(conn) -> conn
end