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
Storage | Feature Flag | Indexing support | Use Cases |
---|---|---|---|
storage::memory::MemoryStorage | Built-in | ❌ | Development, testing |
storage::memory::MemoryStorageIndexed | Built-in | ✅ | Development with indexing features |
storage::cookie::CookieStorage | cookie | ❌ | Client-side storage, stateless servers |
storage::redis::RedisFredStorage | redis_fred | ✅ | Production, distributed systems |
storage::sqlx::SqlxPostgresStorage | sqlx_postgres | ✅ | Production, existing database |
storage::sqlx::SqlxSqliteStorage | sqlx_sqlite | ✅ | Development 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
- Trait bounds: Add additional trait bounds to the session data type
<T>
as needed - Error Handling: Use
error::SessionError::Backend
for custom errors - TTL Handling: Respect the TTL parameters in
load
andsave
for session expiration - Indexing Consistency: Keep identifier indexes in sync with session data
- Cleanup: Implement proper cleanup in
shutdown()
if needed
§Feature flags
These features can be enabled as shown in Cargo’s documentation.
Name | Description |
---|---|
cookie | A cookie-based session store. Data is serialized using serde_json and then encrypted into the value of a cookie. |
redis_fred | A session store for Redis (and Redis-compatible databases), using the fred.rs crate. |
sqlx_postgres | A session store using PostgreSQL via the sqlx crate. |
sqlx_sqlite | A session store using SQLite via the sqlx crate. |
rocket_okapi | Enables support for the rocket_okapi crate if needed. |
Modules§
Structs§
- Rocket
Flex Session - A Rocket fairing that enables sessions.
- Rocket
Flex Session Options - 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§
- Session
Hash Map - Optional trait for sessions with a hashmap-like data structure.
- Session
Identifier - 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.