Tower Sessions
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
- Tower Sessions GitHub - Official repository
- Tower Sessions Documentation - API documentation
- Axum Login - Authentication integration example
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()
}
}
}