Web

Sessions and CSRF

Sessions keep values per visitor on the server; the cookie carries only a random ID. csrf() makes sure a form post really came from your own page.

Turning them on#

Add both to the group that serves pages. JSON APIs usually stay outside it.

use rustclamp::config::Config;
use rustclamp::web::{Router, Sessions, csrf, redirect};

pub fn routes(router: Router, config: &Config) -> Router {
    // SESSION_MINUTES (default 120) and SESSION_SECURE (on in production)
    let sessions = Sessions::from_config(config);
    router.group(|site| {
        site.middleware(sessions.middleware())
            .middleware(csrf())                    // after Sessions: it needs the session
            .get("/contact", contact_form)
            .post("/contact", |request| {
                let name = request.form("name").unwrap_or_default();
                if let Some(session) = request.session() {
                    session.put("flash", &format!("Thanks, {name}!"));
                }
                redirect("/contact")
            })
    })
}

Reading and writing values#

CallDoes
session.get("key")Reads a value
session.put("key", "value")Stores a value
session.take("flash")Reads and removes it: a one-time flash message
session.csrf_token()The session's CSRF token, created on first use
session.regenerate()A new session ID and CSRF token, keeping the values. Call it on login, logout and any change of privilege, so an ID someone planted or saw earlier stops working
session.csrf_field()A ready hidden <input name="_token"> for forms

Values are strings. The cookie is named clamp_session (COOKIE), and every response refreshes it.

CSRF in forms#

csrf() refuses POST, PUT, PATCH and DELETE requests whose _token form field or X-CSRF-Token header does not match the session, with 419 Page Expired. Put the field in every form:

pub fn contact_form(request: &Request) -> Response {
    // Passes csrf, flash, errors and old input to the view.
    request.render("contact", &[])
}
<form method="post" action="/contact">
  {!! csrf !!}
  <input name="name" value="{{ old.name }}" required>
  <button>Send</button>
</form>
@if(flash)<p>{{ flash }}</p>@endif

For fetch calls, send the token in the X-CSRF-Token header instead.

In production#

  • HTTPS only: with Sessions::from_config the cookie is Secure whenever APP_ENV=production, and the app refuses to start if SESSION_SECURE=false there. Sessions::new(…).secure(true) does the same by hand.
  • In memory: sessions live in the server process, so a restart signs everyone out. Keep durable state in a database.