Crate rocket_flex_session

Crate rocket_flex_session 

Source
Expand description

§Overview

Simple, extensible session library for Rocket applications.

  • Session cookies are securely stored and encrypted using Rocket’s built-in private cookies
  • Session guard can be used multiple times during a request, enabling various layers of authentication and authorization through Rocket’s request guard system.
  • Makes use of Rocket’s request-local cache to ensure that only one backend call will be made to get the session data, and if the session is updated multiple times during the request, only one call will be made at the end of the request to save the session.
  • Multiple storage providers available, or you can use your own session storage by implementing the SessionStorage trait.
  • Optional session indexing support for advanced features like multi-device login tracking, bulk session invalidation, and security auditing.

§Usage

While technically not needed for development, it is highly recommended to set the secret key in Rocket. That way the sessions will stay valid after reloading your code if you’re using a persistent storage provider. The secret key is required for release mode.

§Basic setup

use rocket::routes;
use rocket_flex_session::{Session, RocketFlexSession};

// Create a session data type (this type must be thread-safe and Clone)
#[derive(Clone)]
struct MySession {
    user_id: String,
    // ..other session fields
}

#[rocket::launch]
fn rocket() -> _ {
    rocket::build()
        // attach the `RocketFlexSession` fairing, passing in your session data type
        .attach(RocketFlexSession::<MySession>::default())
        .mount("/", routes![login])
}

// use the `Session` request guard in a route handler
#[rocket::post("/login")]
fn login(mut session: Session<MySession>) {
    session.set(MySession { user_id: "123".to_owned() });
}

§Request guard auth

If a valid session isn’t found, the Session request guard will still succeed, but calling Session.get() or Session.tap() will yield None - indicating an empty/uninitialized session. This primitive is designed for you to be able to add your authentication and authorization layer on top of it using Rocket’s flexible request guard system.

For example, we can write a request guard for our MySession type, that will attempt to retrieve the session data and verify whether there is an active session:

use rocket::{
    http::Status,
    request::{FromRequest, Outcome},
    Request,
};
use rocket_flex_session::Session;

#[derive(Clone)]
struct MySession {
    user_id: String,
    // ..other session fields
}

#[rocket::async_trait]
impl<'r> FromRequest<'r> for MySession {
   type Error = &'r str; // or your custom error type

   async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
       // Run the Session request guard (this guard should always succeed)
       let session = req.guard::<Session<MySession>>().await.expect("should not fail");

       // Get the `MySession` session data, or if it's `None`, send an Unauthorized error
       match session.get() {
           Some(my_session) => Outcome::Success(my_session),
           None => Outcome::Error((Status::Unauthorized, "Not logged in")),
       }
    }
 }

 // Use our new `MySession` request guard in a route handler
 #[rocket::get("/user")]
 fn get_user(session: MySession) -> String {
    return format!("Logged in as user {}!", session.user_id);
 }

For more info and examples of this powerful pattern, please see Rocket’s documentation on request guards.

§HashMap session data

If your session data has a hashmap data structure, you can implement SessionHashMap which will add additional helper methods to Session to read and set keys. This is particularly useful if you expect your session data structure to be inconsistent and/or change frequently.

use rocket_flex_session::{Session, SessionHashMap};
use std::collections::HashMap;

#[derive(Clone, Default)]
struct MySession(HashMap<String, String>);

impl SessionHashMap for MySession {
    type Value = String;

    fn get(&self, key: &str) -> Option<&Self::Value> {
        self.0.get(key)
    }
    fn insert(&mut self, key: String, value: Self::Value) {
        self.0.insert(key, value);
    }
    fn remove(&mut self, key: &str) {
        self.0.remove(key);
    }
}

#[rocket::post("/login")]
fn login(mut session: Session<MySession>) {
    let user_id: Option<String> = session.get_key("user_id");
    session.set_key("name".to_owned(), "Bob".to_owned());
    session.remove_key("foobar");
}

§Session Indexing

For use cases like multi-device login tracking or other security features, you can use a storage provider that supports indexing, and then group sessions by an identifier (such as a user ID) using the SessionIdentifier trait:

use rocket::routes;
use rocket_flex_session::{Session, SessionIdentifier, RocketFlexSession};
use rocket_flex_session::storage::memory::MemoryStorageIndexed;

#[derive(Clone)]
struct UserSession {
    user_id: String,
    device_name: String,
}

impl SessionIdentifier for UserSession {
    type Id = String;

    fn identifier(&self) -> Option<Self::Id> {
        Some(self.user_id.clone()) // Group sessions by user_id
    }
}

#[rocket::get("/user/sessions")]
async fn get_all_user_sessions(session: Session<'_, UserSession>) -> String {
    match session.get_all_sessions().await {
        Ok(Some(sessions)) => format!("Found {} active sessions", sessions.len()),
        Ok(None) => "No active session".to_string(),
        Err(e) => format!("Error: {}", e),
    }
}

#[rocket::get("/user/logout-everywhere")]
async fn logout_everywhere(session: Session<'_, UserSession>) -> String {
    match session.invalidate_all_sessions(false).await {
        Ok(Some(n)) => format!("Logged out from {n} sessions"),
        Ok(None) => "No active session".to_string(),
        Err(e) => format!("Error: {}", e),
    }
}

#[rocket::launch]
fn rocket() -> _ {
    rocket::build()
        .attach(
            RocketFlexSession::<UserSession>::builder()
                .storage(MemoryStorageIndexed::default())
                .build()
        )
        .mount("/", routes![get_all_user_sessions, logout_everywhere])
}

§Storage Providers

This crate supports multiple storage backends with different capabilities:

§Available Storage Providers

StorageFeature FlagIndexing supportUse Cases
storage::memory::MemoryStorageBuilt-inDevelopment, testing
storage::memory::MemoryStorageIndexedBuilt-inDevelopment with indexing features
storage::cookie::CookieStoragecookieClient-side storage, stateless servers
storage::redis::RedisFredStorageredis_fredProduction, distributed systems
storage::sqlx::SqlxPostgresStoragesqlx_postgresProduction, existing database
storage::sqlx::SqlxSqliteStoragesqlx_sqliteDevelopment and small-scale deployments

§Custom Storage

To implement a custom storage provider, implement the SessionStorage trait:

use rocket_flex_session::{error::SessionResult, storage::SessionStorage};
use rocket::{async_trait, http::CookieJar};

pub struct MyCustomStorage {}

#[async_trait]
impl<T> SessionStorage<T> for MyCustomStorage
where
    T: Send + Sync + Clone + 'static,
{
    async fn load(&self, id: &str, ttl: Option<u32>, cookie_jar: &CookieJar) -> SessionResult<(T, u32)> {
        // Load session from your storage
        todo!()
    }

    async fn save(&self, id: &str, data: T, ttl: u32) -> SessionResult<()> {
        // Save session to your storage
        todo!()
    }

    async fn delete(&self, id: &str, data: T) -> SessionResult<()> {
        // Delete session from your storage
        todo!()
    }
}

§Adding Indexing Support

To support session indexing, also implement SessionStorageIndexed, and adjust the SessionStorage implementation as follows:

use rocket_flex_session::{error::SessionResult, storage::{SessionStorage, SessionStorageIndexed, SessionIdentifier}};

struct MyCustomStorage;

#[async_trait]
impl<T> SessionStorageIndexed<T> for MyCustomStorage
where
    T: SessionIdentifier + Send + Sync + Clone + 'static,
{
    async fn get_sessions_by_identifier(&self, id: &T::Id) -> SessionResult<Vec<(String, T, u32)>> {
        // Return all sessions (session_id, session_data, session_ttl) for the given identifier
        todo!()
    }
    // etc...
}

// Make sure to also add the following to the `SessionStorage` trait:
#[async_trait]
impl<T> SessionStorage<T> for MyCustomStorage
where
    T: SessionIdentifier + Send + Sync + Clone + 'static, // add the SessionIdentifier trait bound
{
    // ... In the load() and delete() functions, you can access the identifier using data.identifier() ...

    // Add this function (used internally to access the indexing functions)
    fn as_indexed_storage(&self) -> Option<&dyn SessionStorageIndexed<T>> {
        Some(self)
    }
}

§Implementation Tips

  1. Trait bounds: Add additional trait bounds to the session data type <T> as needed
  2. Error Handling: Use error::SessionError::Backend for custom errors
  3. TTL Handling: Respect the TTL parameters in load and save for session expiration
  4. Indexing Consistency: Keep identifier indexes in sync with session data
  5. Cleanup: Implement proper cleanup in shutdown() if needed

§Feature flags

These features can be enabled as shown in Cargo’s documentation.

NameDescription
cookieA cookie-based session store. Data is serialized using serde_json and then encrypted into the value of a cookie.
redis_fredA session store for Redis (and Redis-compatible databases), using the fred.rs crate.
sqlx_postgresA session store using PostgreSQL via the sqlx crate.
sqlx_sqliteA session store using SQLite via the sqlx crate.
rocket_okapiEnables support for the rocket_okapi crate if needed.

Modules§

error
Error types
storage
Storage implementations for sessions

Structs§

RocketFlexSession
A Rocket fairing that enables sessions.
RocketFlexSessionOptions
Options for configuring the session.
Session
Represents the current session state. When used as a request guard, it will attempt to retrieve the session. The request guard will always succeed - if a valid session wasn’t found, the data functions will return None indicating an inactive session.

Traits§

SessionHashMap
Optional trait for sessions with a hashmap-like data structure.
SessionIdentifier
Trait for session data types that allows grouping sessions by an identifier. This enables features like retrieving all sessions for a user or invalidating all sessions when a user’s password changes.