Tower Sessions

authentication-libraryRustsession-managementTowerAxummiddlewaresessionscookiesauthentication

Authentication Library

Tower Sessions

Overview

Tower Sessions is a session management middleware for Rust's Tower and Axum web frameworks. It provides Session as a request extension and is designed with inspiration from Django's session middleware to implement session-based authentication and session management functionality. Session data is securely stored server-side rather than in cookies.

Details

Tower Sessions provides high-performance and ergonomic session management. The session implementation is a transliteration of Django's session middleware semantics, adapted for the Rust ecosystem. Session data persistence is managed by user-provided SessionStore implementations.

Key Features

  • Key-Value Interface: Supports native Rust types
  • JSON Serialization: Automatic handling of Serialize and Deserialize types
  • Session Store Abstraction: Customizable storage backends
  • Session Lifecycle Management: Automatic session initialization and saving
  • Type Safety: Session operations leveraging Rust's type system

Storage Options

  • MemoryStore: In-memory sessions (for development/testing)
  • SqliteStore: SQLite database storage
  • PostgreSQL: PostgreSQL database integration
  • Redis: High-performance distributed session management
  • Custom Store: Customizable through SessionStore trait implementation

Security Features

  • Session Fixation Attack Prevention: Session token rotation upon login
  • Secure Cookies: HttpOnly, Secure, SameSite attribute configuration
  • Expiration Management: Automatic deletion of expired sessions
  • Encryption Support: Session data encryption options

Pros and Cons

Pros

  • High Performance: Fast session processing leveraging Rust's performance characteristics
  • Type Safety: Safety through compile-time type checking
  • Flexibility: Multiple storage backends and customizable options
  • Integration: Complete integration with Tower and Axum ecosystems
  • Security: High security including session fixation attack prevention
  • Modular: Include only necessary features

Cons

  • Rust Only: Limited to Rust language and Tower/Axum ecosystem
  • Learning Curve: Requires understanding of Rust and async/await
  • Ecosystem: Rust web development ecosystem still evolving
  • Configuration Complexity: Requires specific expertise for advanced configuration

Reference Links

Code Examples

Basic Setup

# Cargo.toml
[dependencies]
tower-sessions = "0.14.0"
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }

Basic Usage with Memory Store

use std::net::SocketAddr;
use axum::{response::IntoResponse, routing::get, Router};
use serde::{Deserialize, Serialize};
use time::Duration;
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer};

const COUNTER_KEY: &str = "counter";

#[derive(Default, Deserialize, Serialize)]
struct Counter(usize);

async fn handler(session: Session) -> impl IntoResponse {
    let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
    session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
    format!("Current count: {}", counter.0)
}

#[tokio::main]
async fn main() {
    let session_store = MemoryStore::default();
    let session_layer = SessionManagerLayer::new(session_store)
        .with_secure(false)
        .with_expiry(Expiry::OnInactivity(Duration::seconds(10)));

    let app = Router::new().route("/", get(handler)).layer(session_layer);

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
    axum::serve(listener, app.into_make_service()).await.unwrap();
}

Usage with SQLite Store

use std::net::SocketAddr;
use axum::{response::IntoResponse, routing::get, Router};
use serde::{Deserialize, Serialize};
use time::Duration;
use tower_sessions::{session_store::ExpiredDeletion, Expiry, Session, SessionManagerLayer};
use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore};

const COUNTER_KEY: &str = "counter";

#[derive(Serialize, Deserialize, Default)]
struct Counter(usize);

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // SQLite database connection
    let pool = SqlitePool::connect("sqlite::memory:").await?;
    let session_store = SqliteStore::new(pool);
    session_store.migrate().await?;

    // Automatic deletion task for expired sessions
    let deletion_task = tokio::task::spawn(
        session_store
            .clone()
            .continuously_delete_expired(tokio::time::Duration::from_secs(60)),
    );

    // Session management layer configuration
    let session_layer = SessionManagerLayer::new(session_store)
        .with_secure(false)
        .with_expiry(Expiry::OnInactivity(Duration::seconds(600)));

    let app = Router::new().route("/", get(handler)).layer(session_layer);

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let listener = tokio::net::TcpListener::bind(&addr).await?;
    axum::serve(listener, app.into_make_service()).await?;

    deletion_task.await??;
    Ok(())
}

async fn handler(session: Session) -> impl IntoResponse {
    let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
    session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
    format!("Current count: {}", counter.0)
}

Session-Based Authentication

use axum::{
    http::StatusCode,
    response::{IntoResponse, Redirect},
    routing::{get, post},
    Form, Router,
};
use serde::{Deserialize, Serialize};
use tower_sessions::{MemoryStore, Session, SessionManagerLayer};

const USER_ID_KEY: &str = "user_id";

#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    username: String,
}

#[derive(Deserialize)]
struct LoginForm {
    username: String,
    password: String,
}

// Login handler
async fn login_post(session: Session, Form(form): Form<LoginForm>) -> impl IntoResponse {
    // Authentication check (use proper authentication logic in actual implementation)
    if authenticate_user(&form.username, &form.password).await {
        let user = User {
            id: 1,
            username: form.username,
        };
        
        // Store user information in session
        session.insert(USER_ID_KEY, user.id).await.unwrap();
        
        Redirect::to("/dashboard").into_response()
    } else {
        (StatusCode::UNAUTHORIZED, "Invalid credentials").into_response()
    }
}

// Protected route
async fn dashboard(session: Session) -> impl IntoResponse {
    if let Ok(Some(user_id)) = session.get::<u32>(USER_ID_KEY).await {
        format!("Welcome user {}!", user_id)
    } else {
        Redirect::to("/login").into_response()
    }
}

// Logout handler
async fn logout(session: Session) -> impl IntoResponse {
    session.delete().await.unwrap();
    Redirect::to("/login")
}

async fn authenticate_user(username: &str, password: &str) -> bool {
    // Implement actual authentication logic here
    username == "admin" && password == "password"
}

#[tokio::main]
async fn main() {
    let session_store = MemoryStore::default();
    let session_layer = SessionManagerLayer::new(session_store);

    let app = Router::new()
        .route("/login", post(login_post))
        .route("/dashboard", get(dashboard))
        .route("/logout", post(logout))
        .layer(session_layer);

    // Server startup code...
}

Custom Session Store

use async_trait::async_trait;
use tower_sessions::session_store::{Error, Result, SessionStore};
use tower_sessions::{session::Record, SessionId};

pub struct CustomSessionStore {
    // Custom storage implementation
}

#[async_trait]
impl SessionStore for CustomSessionStore {
    async fn save(&self, session_record: &Record) -> Result<()> {
        // Save session record
        println!("Saving session: {:?}", session_record.id);
        Ok(())
    }

    async fn load(&self, session_id: &SessionId) -> Result<Option<Record>> {
        // Load record from session ID
        println!("Loading session: {:?}", session_id);
        Ok(None)
    }

    async fn delete(&self, session_id: &SessionId) -> Result<()> {
        // Delete session
        println!("Deleting session: {:?}", session_id);
        Ok(())
    }
}

Session Configuration Customization

use time::Duration;
use tower_sessions::{Expiry, SessionManagerLayer, MemoryStore};

let session_layer = SessionManagerLayer::new(MemoryStore::default())
    .with_name("my_session")                    // Session name
    .with_domain("example.com")                 // Domain setting
    .with_http_only(true)                      // HttpOnly cookie
    .with_same_site(tower_sessions::cookie::SameSite::Strict)  // SameSite setting
    .with_secure(true)                         // Secure cookie (HTTPS required)
    .with_path("/app")                         // Cookie path
    .with_expiry(Expiry::OnInactivity(Duration::minutes(30)));  // Expire after 30 minutes of inactivity

Error Handling

use tower_sessions::Session;

async fn safe_session_handler(session: Session) -> impl IntoResponse {
    match session.get::<String>("key").await {
        Ok(Some(value)) => format!("Value: {}", value),
        Ok(None) => "Key not found".to_string(),
        Err(e) => {
            eprintln!("Session error: {:?}", e);
            "Internal server error".to_string()
        }
    }
}