Session Storage Guide
Server-side session storage for Armature framework with Redis, Memcached, and CouchDB backends.
Table of Contents
- Important: Stateless Architecture Preferred
- Overview
- Features
- Installation
- When to Use Sessions
- Quick Start
- Session Backends
- Session API
- Integration with Handlers
- Best Practices
- Migration from Sessions to JWT
- API Reference
- Summary
Important: Stateless Architecture Preferred
โ ๏ธ Armature strongly recommends stateless architecture using JWT tokens instead of server-side sessions.
Why Stateless?
| Aspect | Stateless (JWT) | Sessions |
|---|---|---|
| Scalability | โ Any server handles any request | โ Requires shared session store |
| Performance | โ No session lookups | โ External store on every request |
| Reliability | โ No session store to fail | โ Session store is SPOF |
| Cloud-Native | โ Perfect for K8s/serverless | โ Requires stateful infrastructure |
| Security | โ No session hijacking | โ Session vulnerabilities possible |
Preferred Approach: JWT Authentication
use armature_jwt::JwtManager;
// Create token at login - contains all user info
let token = jwt_manager.create_token(UserClaims {
user_id: user.id,
email: user.email,
roles: user.roles,
})?;
// Client stores token (localStorage, cookie, etc.)
// Server validates on each request - no session lookup needed
let claims = jwt_manager.verify_token(&token)?;
See the Stateless Architecture Guide for comprehensive details.
Overview
The armature-session module provides server-side session storage for cases where sessions are absolutely necessary. It supports multiple backends with a unified API.
Use this module only when:
- Integrating with legacy systems requiring sessions
- Compliance mandates server-side session tracking
- You need immediate session invalidation
- Storing large amounts of temporary user data
Features
- โ Redis backend - High-performance, default option
- โ Memcached backend - Optional, feature-gated
- โ CouchDB backend - Document-based sessions
- โ
Unified
SessionStoretrait across all backends - โ Automatic session expiration
- โ Session metadata (IP, user agent)
- โ Configurable TTL with max limits
- โ Typed session data storage
Installation
Add to your Cargo.toml:
[dependencies]
# Redis only (default)
armature-session = "0.1"
# With Memcached support
armature-session = { version = "0.1", features = ["memcached"] }
# With CouchDB support
armature-session = { version = "0.1", features = ["couchdb"] }
# All backends
armature-session = { version = "0.1", features = ["full"] }
Or via the main armature crate:
[dependencies]
armature-framework = { version = "0.1", features = ["session"] }
# With Memcached
armature-framework = { version = "0.1", features = ["session", "session-memcached"] }
When to Use Sessions
โ Appropriate Use Cases
Legacy System Integration
// Old system expects session IDs let session = store.create(None).await?; legacy_api.set_session_id(&session.id)?;Compliance Requirements
// Audit trail requires server-side session tracking session.set("audit_trail", audit_entry)?; store.save(&session).await?;Immediate Logout (All Devices)
// Delete all sessions for a user for session_id in user_sessions { store.delete(&session_id).await?; }Large Temporary Data
// Shopping cart, multi-step wizard data session.set("cart", large_cart_data)?; session.set("wizard_step", 3)?;
โ Inappropriate Use Cases
- Authentication only โ Use JWT
- Simple user identification โ Use JWT
- API authentication โ Use JWT/API keys
- Microservices โ Use JWT for service-to-service
Quick Start
Redis Session Store (Default)
use armature_session::*;
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), SessionError> {
// Configure Redis session store
let config = SessionConfig::redis("redis://localhost:6379")?
.with_namespace("myapp:session")
.with_default_ttl(Duration::from_secs(3600)) // 1 hour
.with_max_ttl(Duration::from_secs(86400)); // 24 hour max
let store = RedisSessionStore::new(config).await?;
// Create a new session
let mut session = store.create(None).await?;
println!("Session ID: {}", session.id);
// Store typed data
session.set("user_id", 123)?;
session.set("username", "alice")?;
session.set("roles", vec!["admin", "user"])?;
store.save(&session).await?;
// Retrieve session later
if let Some(session) = store.get(&session.id).await? {
let user_id: Option<i32> = session.get("user_id");
let roles: Option<Vec<String>> = session.get("roles");
println!("User: {:?}, Roles: {:?}", user_id, roles);
}
// Delete session (logout)
store.delete(&session.id).await?;
Ok(())
}
Session Backends
Redis (Default)
High-performance, recommended for most use cases.
use armature_session::*;
let config = SessionConfig::redis("redis://localhost:6379")?
.with_namespace("myapp:session")
.with_default_ttl(Duration::from_secs(3600));
let store = RedisSessionStore::new(config).await?;
Pros:
- Very fast (in-memory)
- Automatic expiration (TTL)
- Clustering support
- Persistence options
Cons:
- Additional infrastructure
- Memory-based (limited by RAM)
Memcached (Feature-gated)
Requires memcached feature flag.
[dependencies]
armature-session = { version = "0.1", features = ["memcached"] }
use armature_session::*;
let config = SessionConfig::memcached("memcache://localhost:11211")?
.with_namespace("myapp:session")
.with_default_ttl(Duration::from_secs(3600));
let store = MemcachedSessionStore::new(config).await?;
Pros:
- Simple protocol
- Very fast
- Distributed by design
Cons:
- No persistence (volatile)
- No prefix scanning (count/clear_all limited)
- Memory-only
CouchDB
Document-based sessions for environments already using CouchDB.
[dependencies]
armature-session = { version = "0.1", features = ["couchdb"] }
use armature_session::*;
let config = SessionConfig::couchdb("http://localhost:5984", "sessions")?
.with_namespace("myapp")
.with_default_ttl(Duration::from_secs(3600))
.with_auth("admin", "password");
let store = CouchDbSessionStore::new(config).await?;
Setup CouchDB database:
# Create database
curl -X PUT http://admin:password@localhost:5984/sessions
# Create view for expiration cleanup
curl -X PUT http://admin:password@localhost:5984/sessions/_design/sessions \
-H "Content-Type: application/json" \
-d '{
"views": {
"by_expiration": {
"map": "function(doc) { if(doc.expires_at) emit(doc.expires_at, null); }"
}
}
}'
Pros:
- Persistent storage
- Query capabilities
- Good for document-oriented data
Cons:
- Slower than Redis/Memcached
- More complex setup
- Higher resource usage
Session API
Session Data Structure
pub struct Session {
pub id: String, // Unique session ID
pub data: HashMap<String, serde_json::Value>, // Session data
pub created_at: DateTime<Utc>, // Creation time
pub last_accessed_at: DateTime<Utc>, // Last access time
pub expires_at: DateTime<Utc>, // Expiration time
pub user_agent: Option<String>, // Browser/client info
pub ip_address: Option<String>, // Client IP
}
Session Methods
// Get typed value
let user_id: Option<i32> = session.get("user_id");
// Set typed value
session.set("user_id", 123)?;
// Remove value
session.remove("temp_data");
// Check if key exists
if session.contains("user_id") { ... }
// Get all keys
let keys = session.keys();
// Clear all data
session.clear();
// Update last accessed time
session.touch();
// Extend expiration
session.extend(Duration::from_secs(3600));
// Check if expired
if session.is_expired() { ... }
// Add metadata
let session = Session::new(id, ttl)
.with_user_agent("Mozilla/5.0...")
.with_ip_address("192.168.1.1");
SessionStore Trait
#[async_trait]
pub trait SessionStore: Send + Sync {
// Create new session
async fn create(&self, ttl: Option<Duration>) -> SessionResult<Session>;
// Get session by ID
async fn get(&self, session_id: &str) -> SessionResult<Option<Session>>;
// Save/update session
async fn save(&self, session: &Session) -> SessionResult<()>;
// Delete session
async fn delete(&self, session_id: &str) -> SessionResult<()>;
// Check if session exists
async fn exists(&self, session_id: &str) -> SessionResult<bool>;
// Extend session TTL
async fn extend(&self, session_id: &str, ttl: Duration) -> SessionResult<()>;
// Touch session (update last accessed)
async fn touch(&self, session_id: &str) -> SessionResult<()>;
// Clear all sessions (dangerous!)
async fn clear_all(&self) -> SessionResult<()>;
// Count active sessions
async fn count(&self) -> SessionResult<usize>;
// Cleanup expired sessions
async fn cleanup_expired(&self) -> SessionResult<usize>;
}
Integration with Handlers
Middleware Approach
use armature_framework::prelude::*;
use armature_session::*;
pub struct SessionMiddleware {
store: RedisSessionStore,
}
#[async_trait]
impl Middleware for SessionMiddleware {
async fn handle(&self, mut req: HttpRequest, next: Next) -> Result<HttpResponse, Error> {
// Extract session ID from cookie
let session_id = req.cookie("session_id");
if let Some(id) = session_id {
if let Some(session) = self.store.get(&id).await? {
// Session valid - attach to request
req.headers.insert("x-session-id".to_string(), id);
req.headers.insert("x-user-id".to_string(),
session.get::<String>("user_id").unwrap_or_default());
}
}
next(req).await
}
}
Controller with Sessions
#[controller("/api")]
struct UserController {
session_store: RedisSessionStore,
}
impl UserController {
#[post("/login")]
async fn login(
&self,
#[body] credentials: Body<LoginRequest>,
) -> Result<HttpResponse, Error> {
// Validate credentials...
let user = validate_user(&credentials)?;
// Create session
let mut session = self.session_store.create(None).await?;
session.set("user_id", user.id)?;
session.set("roles", user.roles)?;
self.session_store.save(&session).await?;
// Return session ID in cookie
HttpResponse::ok()
.with_cookie("session_id", &session.id, CookieOptions::secure())
.with_json(&LoginResponse { success: true })
}
#[post("/logout")]
async fn logout(
&self,
#[header("Cookie")] cookie: Header,
) -> Result<HttpResponse, Error> {
if let Some(session_id) = parse_cookie(&cookie, "session_id") {
self.session_store.delete(&session_id).await?;
}
HttpResponse::ok()
.with_cleared_cookie("session_id")
.with_json(&LogoutResponse { success: true })
}
}
Best Practices
1. Always Set Reasonable TTLs
// โ
Good - Short default, enforced maximum
let config = SessionConfig::redis("redis://localhost:6379")?
.with_default_ttl(Duration::from_secs(3600)) // 1 hour
.with_max_ttl(Duration::from_secs(86400)); // 24 hour max
// โ Bad - Very long sessions are security risks
let config = SessionConfig::redis("redis://localhost:6379")?
.with_default_ttl(Duration::from_secs(86400 * 30)); // 30 days!
2. Store Minimal Data in Sessions
// โ
Good - Only essential data
session.set("user_id", user.id)?;
session.set("roles", user.roles)?;
// โ Bad - Full user object with sensitive data
session.set("user", full_user_object)?; // Contains password hash!
3. Use Secure Session IDs
// Session IDs are automatically generated as UUIDs
// Never allow user-provided session IDs
let session = store.create(None).await?; // ID auto-generated
4. Regenerate Sessions on Privilege Changes
async fn elevate_privileges(
store: &impl SessionStore,
old_session_id: &str,
) -> Result<Session, Error> {
// Get old session data
let old_session = store.get(old_session_id).await?.unwrap();
// Delete old session
store.delete(old_session_id).await?;
// Create new session with elevated privileges
let mut new_session = store.create(None).await?;
new_session.set("user_id", old_session.get::<i32>("user_id"))?;
new_session.set("is_admin", true)?;
store.save(&new_session).await?;
Ok(new_session)
}
5. Clean Up Expired Sessions
// Run periodically (cron job)
async fn cleanup_sessions(store: &impl SessionStore) {
match store.cleanup_expired().await {
Ok(count) => println!("Cleaned up {} expired sessions", count),
Err(e) => eprintln!("Session cleanup failed: {}", e),
}
}
Migration from Sessions to JWT
If you're currently using sessions and want to migrate to stateless JWT:
Step 1: Add JWT Support
use armature_jwt::JwtManager;
let jwt_manager = JwtManager::new(JwtConfig {
secret: "your-secret-key",
expiration: Duration::from_secs(3600),
..Default::default()
})?;
Step 2: Dual Authentication Period
async fn authenticate(
req: &HttpRequest,
session_store: &impl SessionStore,
jwt_manager: &JwtManager,
) -> Result<UserId, Error> {
// Try JWT first (new method)
if let Some(token) = extract_bearer_token(req) {
if let Ok(claims) = jwt_manager.verify_token(token) {
return Ok(claims.user_id);
}
}
// Fall back to session (legacy)
if let Some(session_id) = req.cookie("session_id") {
if let Some(session) = session_store.get(&session_id).await? {
return Ok(session.get("user_id").unwrap());
}
}
Err(Error::Unauthorized)
}
Step 3: Issue JWT on Login
async fn login(
credentials: LoginRequest,
jwt_manager: &JwtManager,
) -> Result<LoginResponse, Error> {
let user = validate_user(&credentials)?;
// Issue JWT instead of creating session
let token = jwt_manager.create_token(UserClaims {
user_id: user.id,
roles: user.roles,
})?;
Ok(LoginResponse { token })
}
Step 4: Remove Session Support
Once all clients are migrated, remove session dependencies.
Common Pitfalls
- โ Don't use sessions for simple authentication - use JWT
- โ Don't store sensitive data (passwords, keys) in sessions
- โ Don't use very long session TTLs
- โ Don't trust session IDs from untrusted sources
- โ Do regenerate session ID after login
- โ Do set secure cookie flags
- โ Do implement session cleanup
- โ Do consider JWT for new applications
API Reference
Types
| Type | Description |
|---|---|
Session |
Session data structure |
SessionConfig |
Configuration for session stores |
SessionBackend |
Enum of backend types |
SessionError |
Error type for session operations |
SessionResult<T> |
Result type alias |
Traits
| Trait | Description |
|---|---|
SessionStore |
Main trait for session storage backends |
Structs
| Struct | Feature | Description |
|---|---|---|
RedisSessionStore |
redis (default) |
Redis-backed sessions |
MemcachedSessionStore |
memcached |
Memcached-backed sessions |
CouchDbSessionStore |
couchdb |
CouchDB-backed sessions |
Functions
| Function | Description |
|---|---|
generate_session_id() |
Generate a new UUID session ID |
Summary
Key Takeaways:
- โ ๏ธ Prefer stateless JWT authentication - Sessions add complexity
- Use sessions only when absolutely necessary (legacy, compliance, immediate logout)
- Redis is the recommended backend for performance
- Memcached requires
memcachedfeature flag - Always set reasonable TTLs with enforced maximums
- Store minimal data in sessions
- Consider migrating to JWT for new applications
Quick Reference:
use armature_session::prelude::*;
// Redis (default)
let store = RedisSessionStore::new(
SessionConfig::redis("redis://localhost:6379")?
).await?;
// Create session
let session = store.create(None).await?;
// Store data
session.set("user_id", 123)?;
store.save(&session).await?;
// Retrieve
if let Some(session) = store.get(&session.id).await? {
let user_id: Option<i32> = session.get("user_id");
}
// Delete (logout)
store.delete(&session.id).await?;
Remember: If you're starting a new project, use JWT. Sessions are for legacy compatibility and specific use cases only.