tower-sessions

repository·main·Indexed 19 days ago

https://github.com/maxcountryman/tower-sessions

A middleware crate providing session management for tower and axum applications. It allows developers to associate key-value pairs with visitors using pluggable, high-performance storage backends via the SessionStore trait. Features include a Session extractor for axum handlers, support for native Rust types implementing Serialize, and a CachingSessionStore for layered caching. Supported backends include Redis, MongoDB, SQLx, DynamoDB, and others.

Tokens
8.6K
Snippets
19
Records
26
Agent score
65%

What's inside tower-sessions

  1. How tower-sessions works

    main

    tower-sessions provides key-value pairs associated with a site visitor using a tower middleware.

    Key features include:

    • Pluggable Storage Backends: Decoupled storage via the SessionStore trait.
    • Minimal Overhead: Sessions are only loaded from backing stores when actually used (e.g., in a handler).
    • Axum Integration: Provides a Session extractor for use directly in axum handlers.
    • Key-Value Interface: Supports native Rust types that implement Serialize (JSON-based).
  2. Use tower-sessions with Axum

    main

    To use sessions in an axum application, follow these steps:

    1. Initialize a Store: Create a session store (e.g., MemoryStore).
    2. Configure the Layer: Create a SessionManagerLayer using the store. You can configure security settings and expiry policies.
    3. Add the Layer to your Router: Apply the layer to your axum::Router.
    4. Extract the Session: Use the Session extractor in your handler functions to get and insert data.

    Note: Data stored in the session must implement serde::Serialize and serde::Deserialize.

    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 {
        // Retrieve value from session
        let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
        // Update value in session
        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();
    }
  3. Use the Session and SessionStore traits

    main

    The tower-sessions core library provides the fundamental abstractions for session management.

    • Session: Represents an active user session, providing methods to interact with session data.
    • SessionStore: A trait that defines how sessions are persisted (e.g., in memory, in a database, or via a cache).
    • CachingSessionStore: A wrapper used to add caching capabilities to an underlying SessionStore to improve performance.
    • Expiry: Defines how long a session remains valid.
    use tower_sessions::{Session, SessionStore};
  4. Configure session expiry with the Expiry enum

    main

    You can control how long a session remains valid using the Expiry enum. This can be set via Session::set_expiry.

    Supported expiry modes:

    • OnSessionEnd: The session expires when the browser session ends (as defined by the browser).
    • OnInactivity(Duration): The session expires after a period of inactivity. Note that reading a session is not considered activity; expiration is computed based on the last time the session was modified (e.g., via insert or remove).
    • AtDateTime(OffsetDateTime): The session expires at a specific, absolute date and time.
    use time::{Duration, OffsetDateTime};
    use tower_sessions::Expiry;
    
    // Expire in 5 minutes of inactivity
    let expiry = Expiry::OnInactivity(Duration::minutes(5));
    session.set_expiry(Some(expiry));
    
    // Expire at a specific timestamp
    let expired_at = OffsetDateTime::now_utc().saturating_add(Duration::weeks(2));
    session.set_expiry(Some(Expiry::AtDateTime(expired_at)));
  5. How SessionManagerLayer works

    main

    The SessionManagerLayer is a Tower Layer that wraps your service in a CookieManager containing a SessionManager service.

    Lifecycle:

    1. Request Phase: The middleware extracts the session ID from the incoming request cookies (using the configured CookieController, such as PlaintextCookie, SignedCookie, or PrivateCookie). It then creates a Session object and inserts it into the request's extensions.
    2. Service Execution: Your application logic runs, accessing the Session via request extensions.
    3. Response Phase:
      • If the session was modified, the middleware saves the session to the SessionStore and adds/updates the session cookie in the response.
      • If the session is empty (e.g., after a logout/flush), the middleware removes the session cookie from the response.
      • If always_save is enabled, the session is saved even if no changes were made, which is useful for resetting expiration timers on every request.
  6. How the Session extractor works in Axum

    main

    In axum, the Session type implements FromRequestParts, allowing it to be used as a handler extractor. You can extend this pattern to create custom, strongly-typed extractors by implementing FromRequestParts for your own types. This allows you to encapsulate session logic (like fetching a specific struct from a key) and provide a clean API to your handlers.

    # use async_trait::async_trait;
    # use axum::extract::FromRequestParts;
    # use http::{request::Parts, StatusCode};
    # use serde::{Deserialize, Serialize};
    # use tower_sessions::{SessionStore, Session, MemoryStore};
    const COUNTER_KEY: &str = "counter";
    
    #[derive(Default, Deserialize, Serialize)]
    struct Counter(usize);
    
    impl<S> FromRequestParts<S> for Counter
    where
        S: Send + Sync,
    {
        type Rejection = (http::StatusCode, &'static str);
    
        async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
            let session = Session::from_request_parts(req, state).await?;
            let counter: Counter = session.get(COUNTER_KEY).await.unwrap().unwrap_or_default();
            session.insert(COUNTER_KEY, counter.0 + 1).await.unwrap();
    
            Ok(counter)
        }
    }
  7. Use `CachingSessionStore` to improve read performance

    main

    The CachingSessionStore provides a layered caching mechanism. It uses one SessionStore as a fast frontend (the cache) and another SessionStore as the persistent backend.

    When a session is loaded, the system first checks the cache. If the session is found in the cache, it is returned immediately. If not, it is loaded from the backend and then hydrated into the cache for future requests. This significantly reduces the cost of frequent reads.

    use tower_sessions::CachingSessionStore;
    use tower_sessions_moka_store::MokaStore;
    use tower_sessions_sqlx_store::{SqlitePool, SqliteStore};
    
    let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
    let sqlite_store = SqliteStore::new(pool);
    let moka_store = MokaStore::new(Some(2_000));
    let caching_store = CachingSessionStore::new(moka_store, sqlite_store);
  8. Use CachingSessionStore for layered caching

    main

    If your primary SessionStore is slow (e.g., a database), you can use CachingSessionStore to place a cache in front of it. CachingSessionStore manages both a cache and a backend store. When loading a session, it first checks the cache; if it misses, it loads from the store and populates the cache. This is highly effective for read-heavy workloads.

    # use tower::ServiceBuilder;
    # use tower_sessions::{CachingSessionStore, SessionManagerLayer};
    # use tower_sessions_sqlx_store::{sqlx::PgPool, PostgresStore};
    # use tower_sessions_moka_store::MokaStore;
    # use time::Duration;
    # use tokio_test::block_on;
    
    block_on(async {
        let database_url = std::env::var("DATABASE_URL").unwrap();
        let pool = PgPool::connect(&database_url).await.unwrap();
    
        let postgres_store = PostgresStore::new(pool);
        postgres_store.migrate().await.unwrap();
    
        let moka_store = MokaStore::new(Some(10_000));
        let caching_store = CachingSessionStore::new(moka_store, postgres_store);
    
        let session_service = ServiceBuilder::new()
            .layer(SessionManagerLayer::new(caching_store).with_max_age(Duration::days(1)));
    });
  9. Clean up expired sessions in stores without automatic expiry

    main

    Some session stores (like SQLx or MongoDB) do not automatically delete expired sessions. In these cases, you must manually run a background task to clean up stale data. Use the continuously_delete_expired method provided by the store implementation to run a recurring cleanup task.

    # use tower_sessions::{session_store::ExpiredDeletion};
    # use tower_sessions_sqlx_store::{sqlx::SqlitePool, SqliteStore};
    # use tokio_test::block_on;
    
    block_on(async {
        let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
        let session_store = SqliteStore::new(pool);
        let deletion_task = tokio::task::spawn(
            session_store
                .clone()
                .continuously_delete_expired(tokio::time::Duration::from_secs(60)),
        );
        deletion_task.await.unwrap().unwrap();
    });
  10. Use signed and private cookies for session security

    main

    To prevent session ID tampering or exposure, you can use signed or private cookies. This requires enabling the corresponding features in your Cargo.toml.

    • Signed Cookies: Uses a cryptographic key to sign the cookie value. Use .with_signed(key) on the SessionManagerLayer.
    • Private Cookies: Uses a cryptographic key to encrypt the cookie value. Use .with_private(key) on the SessionManagerLayer.

    Both methods require a tower_cookies::Key.

    use tower_sessions::{cookie::Key, MemoryStore, SessionManagerLayer};
    
    // Generate or load a cryptographically random key >= 64 bytes
    let key = Key::generate();
    
    let session_store = MemoryStore::default();
    // For signed cookies
    let signed_layer = SessionManagerLayer::new(session_store.clone()).with_signed(key.clone());
    
    // For private (encrypted) cookies
    let private_layer = SessionManagerLayer::new(session_store).with_private(key);
  11. Install and use tower-sessions with Axum

    main

    To use sessions in an axum application, you need to create a SessionStore (like MemoryStore), wrap it in a SessionManagerLayer, and add that layer to your Router. You can then use the Session struct as an extractor directly in your handler functions to get and set key-value pairs.

    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 = std::net::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();
    }