Cookbook / Auth and sessions
Cookies, sessions and CSRF
A browser login flow with server-side sessions, signed cookies and CSRF protection.
For browser apps, three middleware work together. CookieManager reads and writes cookies (plain, signed or encrypted), Sessions keeps server-side state keyed by a cookie, and Csrf blocks forged cross-site form posts: every POST, PUT, PATCH and DELETE must echo back the token it was given.
use axeon::cookies::{Cookie, CookieManager, Key};
use axeon::csrf::Csrf;
use axeon::session::{MemoryStore, Sessions};
use axeon::{Html, Response, Server, ServerError};
use serde::Deserialize;
#[derive(Deserialize)]
struct Login {
username: String,
}
fn main() {
let mut app = Server::new();
// Load the key from configuration in production so sessions survive restarts.
app.middleware(CookieManager::new().key(Key::generate()))
.middleware(Sessions::new(MemoryStore::new()).secure(!cfg!(debug_assertions)))
.middleware(Csrf::new());
// The form carries the CSRF token in a hidden field.
app.get("/login", |req| async move {
let token = req.csrf_token().unwrap_or_default().to_string();
Html(format!(
r#"<form method="post" action="/login">
<input type="hidden" name="_csrf" value="{token}">
<input name="username" placeholder="Your name">
<button>Log in</button>
</form>"#
))
});
app.post("/login", |req| async move {
let login: Login = req.form()?;
// Check the credentials here, then start a fresh session.
let session = req.session()?;
session.regenerate(); // a new session ID on login prevents fixation
session.insert("username", login.username)?;
req.cookies().signed().add(Cookie::new("theme", "dark"));
Ok::<_, ServerError>(Response::redirect("/me"))
});
app.get("/me", |req| async move {
let name: Option<String> = req.session()?.get("username");
Ok::<_, ServerError>(match name {
Some(name) => format!("signed in as {name}"),
None => "not signed in".to_string(),
})
});
app.post("/logout", |req| async move {
req.session()?.destroy();
Ok::<_, ServerError>(Response::redirect("/login"))
});
app.listen("127.0.0.1:3000").expect("server failed to start");
}
# Fetch the form (this sets the CSRF cookie), then post it back with the token.
TOKEN=$(curl -s -c jar http://127.0.0.1:3000/login | grep -o 'value="[^"]*"' | cut -d'"' -f2)
curl -s -b jar -c jar -d "_csrf=$TOKEN&username=ada" http://127.0.0.1:3000/login -L
curl -s -b jar http://127.0.0.1:3000/me # signed in as ada
curl -s -o /dev/null -w '%{http_code}\n' -X POST -d 'x=1' http://127.0.0.1:3000/logout # 403: no token
Notes#
- Single-page apps can read the
csrf_tokencookie from JavaScript and send it as theX-CSRF-Tokenheader. APIs that use bearer tokens instead of cookies do not needCsrfat all. MemoryStoreis fine for one instance. For several, implement theSessionStoretrait on Redis or your database.signed()cookies can be read but not altered by the client;private()cookies are also encrypted.- Sessions are marked
Securein release builds, so they are only sent over HTTPS. Serve over TLS, or terminate it at your proxy.