How Sessions and Authentication Work in Topcoat: A Developer's Guide

Topcoat generates cryptographically secure tokens for client-side storage while requiring applications to persist only SHA-256 hashes, creating a stateless session system with built-in CSRF protection.

Sessions and authentication in Topcoat rely on a minimalist, security-first architecture implemented across the crates/topcoat-session directory. According to the tokio-rs/topcoat source code, the framework handles token generation, transport, and validation while delegating all session data persistence to the application layer. This design ensures that raw authentication secrets never touch server-side storage, reducing impact potential in the event of database breaches.

Core Session Architecture

Topcoat's session system revolves around three fundamental primitives defined in crates/topcoat-session/src/token.rs and crates/topcoat-session/src/session.rs.

Tokens and TokenHashes

The framework generates 32-byte cryptographically-secure random values represented by the Token struct. These tokens are held exclusively by clients and are never stored raw on the server.

Instead, applications persist TokenHash values—SHA-256 hashes of the original tokens—as defined in crates/topcoat-session/src/token/hash.rs. This hash-based approach means that even if your database leaks, attackers cannot reconstruct valid session tokens without performing an infeasible brute-force attack against the 32-byte key space.

Session Records

A Session struct contains two fields: token_hash and expires_at. When you initiate authentication, the start function returns this struct for your application to store alongside user records. The framework deliberately omits storage logic, allowing you to use any database or cache backend.

Token Storage Abstraction

The TokenStore trait in crates/topcoat-session/src/token/store.rs abstracts how tokens travel between client and server. The default CookieTokenStore implementation uses hardened cookies with the __Host- prefix, Secure, HttpOnly, and SameSite=Lax attributes, but custom implementations can use headers or alternative transport mechanisms.

Session Lifecycle API

All session operations require a request context &Cx and are implemented in crates/topcoat-session/src/session.rs. The token is read once per request and cached, ensuring that mutations immediately affect subsequent code.

Starting Sessions

The session::start(cx).await? function mints a fresh token, sends it to the client via the configured TokenStore, and returns a Session struct containing the hash and expiration. This operation protects against session fixation attacks by ensuring a new token is generated even if the client presented an existing one.

Validating Requests

Use session::token_hash(cx).await? to retrieve the hash of the token presented with the current request. This returns Option<TokenHash>, allowing you to look up the session in your database and verify it hasn't expired.

Terminating Sessions

Calling session::stop(cx).await? instructs the client to discard its token and returns Option<TokenHash> so you can delete the corresponding database record. This completes the logout lifecycle.

Sliding Expiration and Rotation

For active sessions, session::refresh(cx).await? re-issues the same token with a new expiration timestamp, implementing sliding expiration patterns. When privilege levels change, session::rotate(cx).await? replaces the token entirely, returning a Rotation struct containing both the new Session and the revoked TokenHash for atomic database updates.

Configuration and Setup

Configure sessions using SessionConfig from crates/topcoat-session/src/config.rs. The builder pattern allows customization of cookie names, lifetimes, and token storage backends.

use std::time::Duration;
use topcoat::session::{SessionConfig, cookie::CookieTokenStore};

let config = SessionConfig::builder()
    .token_store(CookieTokenStore::new().name("session_id"))
    .lifetime(Duration::from_hours(24 * 14))
    .build();

Apply sessions to your router using extension traits:

use topcoat::{
    cookie::RouterBuilderCookieExt,
    router::Router,
    session::{RouterBuilderSessionExt, SessionConfig},
};

let router = Router::builder()
    .cookies()
    .sessions(SessionConfig::default())
    .build();

CSRF Protection with OriginLayer

When you enable sessions via .sessions(), Topcoat automatically registers OriginLayer from crates/topcoat-session/src/origin.rs. This middleware validates the Sec-Fetch-Site header (falling back to Origin) for any non-idempotent request method (POST, PUT, DELETE, etc.), rejecting cross-origin attempts with a 403 status unless the origin is explicitly trusted.

Implementation Examples

The following patterns from examples/session/src/main.rs demonstrate complete authentication workflows.

Login Handler

use topcoat::{Result, context::Cx, router::{error::SeeOther, see_other}, session};

#[route(POST "/login")]
async fn login(cx: &Cx) -> Result<SeeOther> {
    // …authenticate user credentials…
    let session = session::start(cx).await?;
    // Store session.token_hash and session.expires_at in your database
    persist_session(cx, &user, &session).await?;
    Ok(see_other("/"))
}

Current User Resolution

use topcoat::{Result, context::Cx, session};

async fn current_user(cx: &Cx) -> Result<Option<User>> {
    let Some(hash) = session::token_hash(cx).await? else {
        return Ok(None);
    };
    // Query your database for the hash; return None if missing or expired
    load_user_by_hash(cx, &hash).await
}

Logout Handler

use topcoat::{Result, context::Cx, router::{error::SeeOther, see_other}, session};

#[route(POST "/logout")]
async fn logout(cx: &Cx) -> Result<SeeOther> {
    if let Some(hash) = session::stop(cx).await? {
        delete_session_record(cx, &hash).await?;
    }
    Ok(see_other("/"))
}

Token Rotation for Privilege Changes

use topcoat::{Result, context::Cx, session};

async fn elevate_privileges(cx: &Cx) -> Result<()> {
    if let Some(rot) = session::rotate(cx).await? {
        // Atomically replace the old hash with the new one
        rekey_session(cx, &rot.revoked, &rot.session).await?;
    }
    Ok(())
}

Summary

  • Topcoat uses a hash-based session model where only SHA-256 hashes of 32-byte tokens are stored server-side, implemented in crates/topcoat-session/src/token/hash.rs.
  • The lifecycle API (start, stop, refresh, rotate) in crates/topcoat-session/src/session.rs provides stateless operations that delegate persistence to your application.
  • Default security features include hardened cookies (__Host- prefix, Secure, HttpOnly, SameSite=Lax) and automatic CSRF protection via OriginLayer.
  • Token rotation allows secure privilege escalation by atomically replacing tokens without breaking session continuity.
  • Zero storage assumptions mean you maintain full control over session data, expiration logic, and database schema.

Frequently Asked Questions

How does Topcoat prevent session fixation attacks?

Topcoat's session::start function always generates a fresh 32-byte token, even if the client already presents a valid token from a previous session. This ensures that authenticating into the application invalidates any pre-existing session identifiers, preventing attackers from fixing session IDs before user login.

Where should I store session data in Topcoat?

Topcoat intentionally provides no built-in session storage. You must implement persistence logic in your application code, typically by storing the token_hash and expires_at fields from the Session struct alongside your user records in a database or cache. This design keeps cryptographic material out of your storage layer while giving you complete control over data retention policies.

What happens if I don't rotate tokens after privilege changes?

If you fail to call session::rotate when escalating user privileges (e.g., after MFA verification or role elevation), the original token remains valid with its original security context. Rotation ensures that tokens issued before privilege changes are cryptographically invalidated, requiring the client to present the new token that reflects the updated authorization state.

How does Topcoat protect against CSRF attacks?

When sessions are enabled, Topcoat automatically inserts OriginLayer from crates/topcoat-session/src/origin.rs into the middleware stack. This layer validates the Sec-Fetch-Site header for state-changing requests, rejecting any cross-origin POST, PUT, or DELETE attempts unless the origin is explicitly whitelisted in the SessionConfig.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →