Middleware, Sessions & CSRF

  1. Middleware
  2. Sessions
  3. CSRF
  4. Next

Middleware

Middleware is a plain Axum middleware function - .middleware(...) on a Router attaches it, and it applies to whatever it’s attached to (.group(...) scopes it to a subset, the whole chain applies it to everything). There’s no named-middleware registry to register into and nothing to look up by string:

route.group("", |r: Router| {
    r.middleware(axum::middleware::from_fn(require_auth))
        .get("/posts/create", PostController::create)
})

Write your own with xr make:middleware EnsureSubscribed, which generates the standard Axum middleware-function shape (async fn(request, next) -> Response) for you to fill in.

Sessions

.with_sessions(pool, secure_cookie) attaches session support to a router - called once, on the final, fully-merged router (not inside a .group(...) closure, since it’s async and every route-building closure is not). A generated app’s serve() does this for you already:

route.with_sessions(larust_support::orm::pool()?, app.config().session_secure_cookie).await?

Sessions are backed by tower-sessions, stored in the same database DB_CONNECTION points at (a hand-written SessionStore over sqlx::AnyPool, since third-party per-backend store crates need a concretely-typed pool Larust’s runtime-generic pool can’t give them). This is a deliberate choice, not a limitation waiting to be lifted: session data needs to survive a process restart - a deploy, a crash, xr dev’s own rebuild-and-restart cycle - so there’s no in-memory store in the public API at all. An in-memory session store that quietly logs every user out on every deploy is a well-known Laravel footgun (SESSION_DRIVER=array reaching production); Larust just doesn’t offer the option.

A handler reads/writes the session through the Session extractor - tower_sessions::Session, re-exported directly:

pub async fn store(session: Session, ...) -> Result<impl IntoResponse, AppError> {
    session.insert("user_id", user.id).await?;
    let flash: Option<String> = session.remove("success").await?;
}

The session cookie’s Secure attribute is silently dropped on any hostname a browser doesn’t recognize as a secure context - only 127.0.0.1, ::1, and the literal localhost qualify over plain HTTP. A custom local-dev hostname (a .test domain in /etc/hosts, even one that resolves to loopback) will look fine in the browser but silently receive no session cookie at all - every state-changing request then fails CSRF verification with no error pointing at the real cause. Set SESSION_SECURE_COOKIE=false in .env if you develop against a custom hostname rather than localhost.

CSRF

larust_http::csrf::verify is the middleware; a generated app’s routes/web.rs applies it once, to the whole router, at the very end of the chain - and deliberately not to routes/api.rs, since CSRF protects cookie-authenticated browser form submissions, and an API route merged in via .merge() is immune to whatever middleware the router it’s merged into carries (see Routing).

In a template, @csrf expands to a hidden input carrying the current session’s token:

<form method="post" action="/posts">
    @csrf
    <input name="title">
</form>

Equivalently, for a JS-driven request (a fetch() call, a file upload) where a hidden form field isn’t natural, send the token as a header instead - checked before the body, matching Laravel’s own convention:

fetch("/posts", { method: "POST", headers: { "X-CSRF-TOKEN": token } })

Get the current token explicitly (e.g. to embed in a <meta> tag for JS to read, as the reference layout does) with:

let csrf_token = larust_http::csrf::token(&session).await;

A request that fails verification gets a real 419 page with a link home, not a bare “CSRF token mismatch” string - xr dev’s own auto-reload used to be able to trigger this spuriously by resubmitting a stale POST on reconnect; that’s fixed, but the page itself stayed friendlier since a genuine mismatch (an expired tab, a forged request) can still reach it.

Next

Error Handling covers what happens when any of this fails - AppError, APP_DEBUG, and the default error pages.