Auth & sessions

AkurAI/Framework ships a reference authentication feature: cookie-based sessions on the embedded B+tree, a tiny users table, a plain-HTML login form, a protected page, and logout. It is built the same way as the forms demo — validate, store, redirect — and stores nothing in memory, so logins survive a restart.

This is a framework demo, not a hardened auth system. The PRNG and the password hash are deliberately simple and std-only. Read the security caveats before shipping anything real on top of it.

The session model

A session is a row in data/sessions.db, keyed by an opaque session id and holding the username it belongs to plus a created-at timestamp (unix seconds). The id is the only thing the browser ever sees — it is the cookie value.

Users live in a separate data/users.db, keyed by username, storing a salted password hash (never the plaintext). A demo user is seeded on first boot.

| Store | File | Key | Value | | --- | --- | --- | --- | | Sessions | data/sessions.db | opaque session id | { username, created } | | Users | data/users.db | username | { salt, hash } |

Both are Arc<Mutex<BTree>> handles — the same durable, single-writer storage engine the guestbook uses. Every write commits, so a session is durable the moment login returns.

The login / logout / protected-route flow

GET  /login    → render the login form
POST /login    → validate credentials
                 ├─ ok   → create session, Set-Cookie, 303 → /account
                 └─ fail → re-render the form with an error (username kept)
GET  /account  → if a valid session cookie is present, show the user
                 otherwise 303 → /login
POST /logout   → delete the session, expire the cookie, 303 → /login

The flow is pure progressive enhancement: ordinary <form method="post"> elements, Post/Redirect/Get so a refresh never resubmits, and no JavaScript required. An already-signed-in visitor hitting /login is bounced straight to /account.

The session middleware helper

auth::State::current_user(req) is the middleware/helper that gates protected routes. It reads the Cookie request header, parses out the session value, looks it up in the session store, and returns the username (or None). The /account handler is just:

let Some(username) = state.current_user(req) else {
    return Reply::Response(redirect("/login"));
};

The cookie

On success the server sends:

Set-Cookie: session=<id>; HttpOnly; Path=/; SameSite=Lax

| Attribute | Why | | --- | --- | | HttpOnly | JavaScript cannot read the cookie, blunting XSS session theft. | | Path=/ | The session applies to the whole site. | | SameSite=Lax | The cookie rides top-level navigations but not cross-site sub-requests, a baseline CSRF mitigation. |

Logout sends the same cookie with an empty value and Max-Age=0, so the browser drops it immediately, and deletes the row from data/sessions.db.

The demo user

| Username | Password | | --- | --- | | demo | akurai |

Seeded on first boot via an idempotent ensure_user — re-running the server never resets an existing user's password. Visit /login and sign in to see the protected /account page.

Security caveats

This feature exists to show the shape of session auth on the framework's own storage engine, with zero external crates. It is not production-grade:

SystemTime nanos mixed with a per-process atomic counter and the pid, run through a SplitMix64 diffuser. This is CSPRNG-lite, not a cryptographically secure RNG. For real deployments, source session ids from OS randomness.

SHA-1 (reusing the WebSocket handshake's SHA-1). It is salted and iterated, but SHA-1 is fast and not memory-hard — this is not bcrypt or argon2 and must not be described as such.

Secure, because in the reference deployment HTTPS is terminated by nginx in front of the app. The session cookie's confidentiality in transit relies on that edge TLS layer; if you expose the app directly, add Secure and serve over HTTPS.

add idle/absolute timeouts and id rotation on privilege change.