March Docs

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
end

Login:

conn |> Auth.log_in(user.id)
conn |> Auth.log_in(user.id, remember_token: token)

Logout:

conn |> Auth.log_out()

Functions

fnauthenticatedauthenticated(conn, handler)#

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)
fncurrent_usercurrent_user(conn)#

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"
    end
fnload_current_userload_current_user(conn, loader)#

Load 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)
fnload_current_user_with_rememberload_current_user_with_remember(conn, loader, remember_verifier)#

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
    )
fnlog_inlog_in(conn, user_id, remember_token)#

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)
fnlog_outlog_out(conn, token_deleter)#

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))
fnredirect_after_loginredirect_after_login(conn, default_path)#

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")
fnrequire_authrequire_auth(conn)#

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