maxcountryman / tower-sessions Goto Github PK
View Code? Open in Web Editor NEW๐ฅ Sessions as a `tower` and `axum` middleware.
License: MIT License
๐ฅ Sessions as a `tower` and `axum` middleware.
License: MIT License
This is related to the same problem we had in axum_login
- since cargo
tries to resolve all the dependencies (even the one behind features) immediately, having lots of dependencies with non-additive/conflicting sub-dependencies can cause problems.
An example problem would be trying to implement stores for sqlx
and rusqlite
which share a libsqlite3-sys
dependency - either we make them both happy by having the lowest common denominator for both crates or we can't do anything.
Dividing the stores into separate crates would solve this problem completely.
There is nothing preventing id collisions when creating a new session. More discussion here: #169.
main is affected
0.11.1 confirmed
Presumably all platforms.
Confirmed on Fedora Rawhide Linux as of 2024/03/18.
tower-sessions
tower-sessions-core
When session ids are generated, nothing confirms that there isn't a collision. This is bad as it could allow the one session to take on the identity of another if it ever happens. Considering that a session id is a randomly generated 2^128 value, the chance of a collision is low (assuming good random data). However, the fallout of a collision is pretty high. In my research, I found that at least PHP has logic to prevent id collisions. I think this is important for the security of this project, so I think we should have this.
We are working on hashing out the design here: #171.
Hi,
I'd wanted to implement a session backend for our orm, but I've encountered some "issues" with the provided types, that could be imho improved.
Our orm has the ability to query directly for the Uuid
type and already unpacks it. So it would be nice to have a method to retrieve the inner type of SessionId
directly. The same problem is the other way around, when loading an existing Session. The SessionId
can only be constructed by String
or &str
and involves calling to_string()
on an already existing Uuid
only to be put in Uuid::parse_str
.
use std::collections::HashMap;
use std::str::FromStr;
use axum::async_trait;
use rorm::fields::types::Json;
use rorm::{FieldAccess, Model};
use time::OffsetDateTime;
use tower_sessions::session::SessionId;
use tower_sessions::{Session, SessionRecord, SessionStore};
use uuid::Uuid;
/// The model for the database
#[derive(Model)]
pub struct DBSession {
#[rorm(primary_key)]
uuid: Uuid,
expiration_time: Option<OffsetDateTime>,
data: Json<HashMap<String, serde_json::Value>>,
}
#[derive(Clone)]
pub struct DBSessionStore(rorm::Database);
#[async_trait]
impl SessionStore for DBSessionStore {
type Error = rorm::Error;
async fn save(&self, session_record: &SessionRecord) -> Result<(), Self::Error> {
let uuid_str = session_record.id().to_string();
let uuid = Uuid::from_str(&uuid_str).unwrap();
let mut tx = self.0.start_transaction().await?;
let count = rorm::query!(&mut tx, (DBSession::F.uuid.count(),))
.condition(DBSession::F.uuid.equals(uuid))
.one()
.await?
.0;
let data = Json(session_record.data());
let expiration_time = session_record.expiration_time();
if count != 0 {
rorm::update!(&mut tx, DBSession)
.condition(DBSession::F.uuid.equals(uuid))
.set(DBSession::F.expiration_time, expiration_time)
.set(DBSession::F.data, data)
.exec()
.await?;
} else {
rorm::insert!(&mut tx, DBSession)
.return_nothing()
.single(&DBSession {
uuid,
expiration_time,
data,
})
.await?;
}
tx.commit().await?;
Ok(())
}
async fn load(&self, session_id: &SessionId) -> Result<Option<Session>, Self::Error> {
let uuid_str = session_id.to_string();
let uuid = Uuid::from_str(&uuid_str).unwrap();
let session = rorm::query!(&self.0, DBSession)
.condition(DBSession::F.uuid.equals(uuid))
.optional()
.await?
.map(|x| SessionRecord::from(x).into());
Ok(session)
}
async fn delete(&self, session_id: &SessionId) -> Result<(), Self::Error> {
let uuid_str = session_id.to_string();
let uuid = Uuid::from_str(&uuid_str).unwrap();
rorm::delete!(&self.0, DBSession)
.condition(DBSession::F.uuid.equals(uuid))
.await?;
Ok(())
}
}
impl From<DBSession> for SessionRecord {
fn from(value: DBSession) -> Self {
Self::new(
SessionId::try_from(value.uuid.to_string()).unwrap(),
value.expiration_time,
value.data.into_inner(),
)
}
}
While it works as intended and documented, if you set an Expiry::AtDateTime, all sessions after that date time will be issued with negative max-age. This means no sessions will be issued after that as they have already expired
The story behind this comment is that I configured it like AtDateTime(now + 5 minutes)
, which means my server will stop issuing sessions after 5 minutes of starting, and I failed to realize that it was indeed not a desirable behavior. I think other users may make the same mistake and thus could be useful to document it with a warning (or just remove it from the use cases)
I encountered an issue in 0.2.0
where removing a value from the session would not be considered a change and therefore not saved back into the store.
This problem only shows when the removed value was the very last one in the session's data, leaving it empty.
Here is a minimal example of the problem:
use tower_sessions::Session;
fn main() {
let session = Session::default();
assert!(!session.modified());
session.insert("foo", 42);
assert!(session.modified());
// ^^^^^^^^^^^^^^^^^^^ returns `true`, as expected
session.remove_value("foo");
assert!(session.modified());
// ^^^^^^^^^^^^^^^^^^^ suddenly returns `false`, despite the internal `modified` flag being set
}
Other methods like session.clear()
and session.flush()
can trigger this issue.
My use case was that I was removing a user_id
field from the session to implement a logout endpoint.
This issue caused the logout button to effectively do nothing, since the removal wasn't persisted anymore.
The examples all have this code (simplified):
let error_layer = HandleErrorLayer::new(|_: BoxError| async { StatusCode::BAD_REQUEST });
But this gives the error:
type inside async
block must be known in this context
cannot infer type for type parameter T
declared on the struct HandleErrorLayer
I don't know exactly whether this is a good title for the issue but I'm wondering how you manage to run tests for all databases without the hassle of setting them all up separately.
` ```
let session_store = PostgresStore::new(pool);
session_store.migrate().await.unwrap();
let deletion_task = tokio::task::spawn(
session_store
.clone()
.continuously_delete_expired(tokio::time::Duration::from_secs(60)),
);
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnInactivity(Duration::seconds(10))),
);
let app = routes::web().layer(session_service).with_state(state);`
I have 2 errs
error[E0599]: no method named continuously_delete_expired
found for struct PostgresStore
in the current scope
--> src/lib.rs:87:14
|
85 | / session_store
86 | | .clone()
87 | | .continuously_delete_expired(tokio::time::Duration::from...
| | -^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in PostgresStore
| |_____________|
|
error[E0277]: the trait bound HandleError<tower_cookies::service::CookieManager<SessionManager<Route, PostgresStore>>, {closure@src/lib.rs:91:38: 91:51}, _>: Service<axum::http::Request<_>>
is not satisfied
--> src/lib.rs:100:35
|
100 | let app = routes::web().layer(session_service).with_state(state);
| ----- ^^^^^^^^^^^^^^^ the trait Service<axum::http::Request<_>>
is not implemented for HandleError<tower_cookies::service::CookieManager<SessionManager<Route, PostgresStore>>, {closure@src/lib.rs:91:38: 91:51}, _>
tower-sessions v0.10.4
tower-sessions-core v0.10.4
Windows 10 64-bit
When inserting data to the session the .insert() function reads the previously stored data before inserting the new data. While this generally is not an issue issues may arise if bad data gets saved into the session.
However, if the read errors with a decode error the new data ends up never getting inserted and instead the insert function just errors on decoding the old data.
Generally I think the insert function should either ignore possible decode issues or there should be an insert function which doesnt even attempt to read the previous session.
The same issue also kind of applies to .remove() as it also reads the previous value which means that
Hi,
is there a particular reason that continuously_delete_expired
takes core's Duration and Expiry::OnInactivity
taking time::Duration
?
My example code:
let store = DBSessionStore {};
tokio::spawn(async move {
store
.continuously_delete_expired(Duration::from_secs(5 * 60))
.await
});
let app = Router::new()
.layer(
ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::INTERNAL_SERVER_ERROR
}))
.layer(
SessionManagerLayer::new(store)
.with_expiry(Expiry::OnInactivity(time::Duration::new(-1440 * 60, 0))),
),
);
In my opinion it would make more sense to take core::time::Duration
in the public API, because:
time
time::Duration
accepts negative durations which wouldn't make sense in an expiry settingI haven't looked through the complete public API, but I'd suggest unifiying the Durations where it make sense.
tower-sessions v0.11.1
tower-sessions-core v0.11.1
tower-sessions-memory-store v0.11.1
Windows 10 64-bit
When using set_expiry
on a session that was created through the SessionManagerLayer without a default expiry the max-age attribute of the cookie is not set. This seems to be because of the match condition in build_cookie()
, which considers the expiry set for SessionManagerLayer and not for the session:
if !matches!(self.expiry, Some(Expiry::OnSessionEnd) | None) {
cookie_builder = cookie_builder.max_age(expiry_age);
}
Example:
use std::net::SocketAddr;
use axum::{response::IntoResponse, routing::get, Router, middleware, middleware::Next, http::{Request, Response}, body::Body as AxumBody};
use time::{OffsetDateTime, Duration};
use tower_sessions::{Expiry, Session, SessionManagerLayer};
use sqlx::sqlite::SqlitePoolOptions;
use tower_sessions_sqlx_store::SqliteStore;
async fn change_expiry(
session: Session,
request: Request<AxumBody>,
next: Next,
) -> Response<AxumBody> {
//modify the session, so it gets stored
session.insert("last_accessed", OffsetDateTime::now_utc()).await.unwrap();
// change the session expiry
let expired_at = OffsetDateTime::now_utc().saturating_add(Duration::seconds(2));
session.set_expiry(Some(Expiry::AtDateTime(expired_at)));
let response = next.run(request).await;
response
}
#[tokio::main]
async fn main() {
simple_logger::init_with_level(log::Level::Warn)
.expect("couldn't initialize logging");
let pool = SqlitePoolOptions::new()
.connect("sqlite:database.db")
.await
.expect("Could not make pool.");
let session_store = SqliteStore::new(pool.clone());
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false);
let app = Router::new().route("/", get(handler))
.layer(middleware::from_fn(change_expiry))
.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();
}
async fn handler() -> impl IntoResponse {
format!("HI")
}
I expected to see this happen:
The max-age attribute of the cookie is set to two seconds.
Instead, this happened:
The max-age of the cookie is Session. In the database the expiry time was correctly updated, this resulted in a warning on reload:
WARN [tower_sessions_core::session] possibly suspicious activity: record not found in store
As I understand it, right now, even if a session was modified, it's expiration_time
is not updated automatically. As a result, end user will lose their session even if they were actively interacting with web server and their session was modified frequently.
To avoid that, current API provides set_expiration_time
and set_expiration_time_from_max_age
methods. This is useful, but I think the crate can provide some additional functionality to make common use cases easier.
First proposition is to allow devs to opt-in for auto-prolonging of the modified sessions, meaning that expiration_time
of sessions marked as modified
should be set to now + the duration set with SessionManagerLayer::with_max_age
. This could be enabled with a new method SessionManagerLayer::with_auto_prolong(bool)
.
Second proposition is to add a new method for session, namely Session::prolong()
that will allow to manually do the same thing in case the session itself was not modified, but the user it's linked to (for example) was. It's just a convenient alternative to Session::set_expiration_time_from_max_age
that doesn't require to move max_age
'
s Duration
to a constant in our own program.
Though there's a small problem with this approach. Consider that dev used set_expiration_time*
method with auto-prolong enabled. The session is marked as modified, therefore session manager will rewrite the expiration_time
set by dev. I think that the best way to solve that is to set expiration_time
to a max(session.expiration_time, now+max_age)
.
Many client side apps log the user out after some time. I think they do this by checking if the session cookie is in the cookies (if its not httponly) or they check the existence of a some companion cookie. The purpose of the companion cookie is to verify if the session cookie exists without having access to the session cookie (it will have the same expiration and not be httponly).
The only way to do this with tower-sessions as is, is to add a companion cookie yourself or wait until your backend responds with 401. This would be a bit simpler and I think it would be a nice feature.
Suggested API Code Example:
SessionManagerLayer::new(session_store).with_max_age(DURATION).with_name(SESSION_COOKIE_NAME).with_companion(true);
If with_companion is called without with_max_age, then it should have no effect and a warning should be logged.
I think this would be a useful feature and simple to implement. If this is a good idea, I can implement it when I get some time.
I'm opening this issue as I still wait on your answer regarding to #59. It is quite frustrating to be promised an answer soonish and not getting a response afterwards.
////////////////// SESSION //////////////////////////////
let session_store = PostgresStore::new(pool.clone());
session_store.migrate().await.unwrap();
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_name("sid")
.with_expiry(Expiry::OnInactivity(Duration::seconds(1000))),
);
I do not see any db tables
although the session get / set works ?
When writing my own SessionStore
based on deadpool-postgres
I found that SessionStore::delete
is called for every new session. Even those which have not been saved. This results in needless DB roundtrips for non-logged-in users:
This is the log output for such request:
2023-11-14T21:31:04.045718Z TRACE session_middleware: tower_sessions::service: loaded_sessions=0
2023-11-14T21:31:04.045790Z DEBUG session_middleware: tower_sessions::service: created new session
2023-11-14T21:31:04.045937Z DEBUG session_middleware{session.id="6ef1555a-c7f1-4027-a539-c12dca148d6e"}: tower_sessions::service: deleted
Those outputs are generated here...
tower-sessions/tower-sessions-core/src/service.rs
Lines 165 to 170 in 2749adb
...and here:
tower-sessions/tower-sessions-core/src/service.rs
Lines 180 to 185 in 2749adb
Looking at the Session.deleted()
implementation I can see why this is happening. The current implementation doesn't keep track if the session was every saved. This results in SessionStore::delete
calls for every request that doesn't modify the session.
Hi,
It's not clear in the documentation whether MemoryStore
handles clean up of stale sessions. Section "Extractor pattern" does not clearly indicate a way to configure SessionManagerLayer
to set expiration of the extracted Session
object.
Would appreciate any guidance on this.
This crate is suppose to replace the axum-sessions crate - which is now deprecated. But as far as I can tell, axum-sessions supports storing sessions in signed cookies (https://docs.rs/async-session/latest/async_session/struct.CookieStore.html), but this library does not.
My axum application does not have any kind of database, and uses oidc to authenticate. Adding a database just to keep track of the login would appear to be an overkill.
At the present, it would appear that the MemoryStore is my only solution, but presumably that will lose all sessions on restarting my program.
This is missing from our current implementation.
As per this comment.
Note: This may require moving expiration_time
into the Inner
wrapper.
I am using https://github.com/maxcountryman/tower-sessions/blob/main/examples/moka-postgres-store.rs
Without the moka store, inserting then fetching the value is no problem, everything is updated as it should, however the design of insert must invalidate the cache, right now I tried to use the example and session.insert
and later in another route fetching the data gets you the stale cached value.
When I try to instantiate a session layer with Redis, I get the following error
the trait bound `RedisStore<RedisPool>: SessionStore` is not satisfied
the following other types implement trait `SessionStore`:
MemoryStore
CachingSessionStore<Cache, Store>
The issue only apear with version 12, 11 works fine.
As far as i understand it the problem arises here
#[derive(Debug, Clone)]
pub struct SessionManagerLayer<Store: SessionStore, C: CookieController = PlaintextCookie> {
session_store: Arc<Store>,
session_config: SessionConfig<'static>,
cookie_controller: C,
}
tower-sessions = { version = "0.12.0", features = ["signed", "private"]}
tower-sessions-redis-store = { version = "0.11.0"}
fred = { version = "7.0.0" }
axum-login = "0.15.0"
<!--
List the versions of all `tower-sessions` crates you are using. The easiest way to get
this information is using `cargo tree`:
-->
MACOS Apple silicone
tower-sessions = { version = "0.12.0", features = ["signed", "private"]}
tower-sessions-redis-store = { version = "0.11.0"}
fred = { version = "7.0.0" }
axum-login = "0.15.0"
whene using the latest versions of the listed crates I get this error.
Reverting all down to the previous major solves the problem
Example:
This works.
tower-sessions = { version = "0.11.1", features = ["signed", "private"]}
tower-sessions-redis-store = { version = "0.11.0"}
fred = { version = "7.0.0" }
axum-login = "0.14.0"
This fails
tower-sessions = { version = "0.12.0", features = ["signed", "private"]}
tower-sessions-redis-store = { version = "0.11.0"}
fred = { version = "7.0.0" }
axum-login = "0.15.0"
Code used:
// pub async fn create_session() -> SessionManagerLayer<RedisStore<RedisPool>, SignedCookie> {
pub async fn create_session() -> SessionManagerLayer<RedisStore<RedisPool>> {
let key = Key::generate();
print!("Session key {:?}", key);
let session_store = RedisStore::new(database::redis::new_pool().await);
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(true)
.with_expiry(Expiry::OnInactivity(Duration::seconds(config::SETTINGS.security.session_expiry)));
// .with_signed(key);
session_layer
}
use fred::clients::RedisPool;
use time::Duration;
// use tower_cookies::Key;
use tower_sessions::{Expiry, SessionManagerLayer};
use tower_sessions_redis_store::RedisStore;
use super::database;
use crate::core::config;
use tower_sessions::{cookie::{Cookie, Key}, service::SignedCookie};
Currently it's hardcoded to tower.sid
here:
Line 56 in 0f953c9
Having a specific session name leaks information about the backend implementation. This should be set to something generic by default (like "id") and preferably changeable by the user (see the OWASP requirements: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#session-id-name-fingerprinting).
Ideally we would not load sessions from stores until the moment we actually want to use them.
Currently we pay an overhead price of loading the session from the backing store on each request where this middleware is installed. This can be mitigated with caching techniques, for example by using Moka
. That said, it's overhead that could be avoided altogether.
Consider that many applications will install this middleware at some lower point in their route graph where only a subset of those routes will use the session--this is an example of overhead we should strive to eliminate altogether.
To facilitate this, I'm working on strategy for loading sessions on demand. The current approach involves making the session generic over a store. This would allow us to defer the store load until we interact with the session in some way, say by explicitly loading or saving it.
These methods will be abstracted away such that as a user the interface remains the same, with the exception that the session takes a generic parameter. Internally, the session will manage a kind of cache which is hydrated lazily and sent back to the store when modifications have happened.
This should also simplify the design measurably and perhaps even enable other optimizations, such as a custom future type (i.e. eliminating the need for a boxed future).
error[E0282]: type annotations needed
--> src/lib.rs:86:16
|
86 | .layer(HandleErrorLayer::new(|_: BoxError| async {
| ^^^^^^^^^^^^^^^^^^^^^ cannot infer type of the type parameter `T` declared on the struct `HandleErrorLayer`
|
help: consider specifying the generic arguments
|
86 | .layer(HandleErrorLayer::<_, T>::new(|_: BoxError| async {
| ++++++++
following redis example
I am trying to make a website with Leptos and Axum that will require users to login to access data. I have got tower-sessions working on a Axum server just fine, but when I introduce Leptos for the front end I get errors.
I used the Leptos Axum Starter Template to get a starting web app up and going.
This is my Cargo.toml file.
Cargo.zip
My rustc version: rustc 1.76.0-nightly (6cf088810 2023-11-26)
cargo version: cargo 1.76.0-nightly (9b13310ca 2023-11-24)
And last but not least here is my main function:
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::{routing::{post, get, put, delete}, Router};
use leptos_axum::{generate_route_list, LeptosRoutes};
use spec_website::app::{*};
use fileserv::file_and_error_handler;
use tower::ServiceBuilder;
use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer};
simple_logger::init_with_level(log::Level::Debug).expect("couldn't initialize logging");
let session_store = MemoryStore::default();
let session_service = ServiceBuilder::new()
// Removed this part because it gave an error.
// Instead I just did this: .layer(axum::Extension(session_service))
// .layer(HandleErrorLayer::new(|_: BoxError| async {
// StatusCode::BAD_REQUEST
// }))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnSessionEnd),
);
let conf = get_configuration(None).await.unwrap();
let leptos_options = conf.leptos_options;
let addr = leptos_options.site_addr.clone();
let routes = generate_route_list(App);
// build our application with a route
let app = Router::new()
.route("/api/auth", post(api::handlers::login_user))
.layer(axum::Extension(session_service))
.leptos_routes(&leptos_options, routes, App)
.fallback(file_and_error_handler)
.with_state(leptos_options);
// run our app with hyper
// 'axum::Server' is a re-export of 'hyper::Server'
log::info!("listening on http://{}", &addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
I migrated to tower-sessions a couple of days ago, but as soon as tower-sessions landed production my server became very slow (even after multiplying the number of instances by 8x and the database capacity by 4x). Once I switched back to axum-sessions, it became fast as usual. Interestingly enough, the production servers didn't show high CPU/RAM usage, and the database was also as usual. My best guess from this behaviour is that axum-sessions uses an async RwLock (https://github.com/maxcountryman/axum-sessions/blob/799e268143a74bb03e772ae81f8391a5f68d8473/src/session.rs#L39), while tower-sessions uses a synchronous Mutex (
Line 40 in d4741d2
The reason why I suspect this is that when used with Axum (which is asynchronous), the synchronous Mutex blocks the event loop, preventing other asynchronous tasks from making progress. Additionally, commonly (or at least in my case), almost all my endpoints read the session, and very few write to the session, so a RwLock is much more performant as there is nothing to wait for. Plus axum-sessions had a ReadableSession and WritableSession extractor, which also boosts performance by segregating writes from reads
Hello
First of all - Thanks for your work on that crate!
I just updated to the latest release (0.5.0) and I keep getting this missmatched types compiler error "expected SessionManagerLayer<_>
, found SessionManagerLayer<MemoryStore>
".
My program works with 0.4.3 version of tower-sessions. I'm new to Rust and thought I was doing something wrong when instantiating a session, but then I tried the example and it yields the same error when bumping to 0.5.0.
error[E0308]: mismatched types
--> examples/sqlite/src/web/app.rs:49:51
|
49 | .layer(AuthManagerLayer::new(backend, session_layer));
| --------------------- ^^^^^^^^^^^^^ expected `SessionManagerLayer<_>`, found `SessionManagerLayer<MemoryStore>`
Any pointers to where I should look to resolve that error are much appreciated.
Thanks!
I have implemented PostgresStore
with tokio-postgres
and bb8-postgres
libraries. I hope you can consider this for future implementation. Given below the code:
use axum::async_trait;
use bb8::{Pool, RunError};
use bb8_postgres::PostgresConnectionManager;
use time::OffsetDateTime;
use tokio_postgres::NoTls;
use tower_sessions::{session, session::Id, session::Session, ExpiredDeletion, SessionStore};
// Alias to represent a database pool connections
pub type DBPool = Pool<PostgresConnectionManager<NoTls>>;
// Alias to error
pub type PostgresConnectionError = RunError<tokio_postgres::error::Error>;
#[derive(thiserror::Error, Debug)]
pub enum PostgresStoreError {
/// A variant to map session errors.
#[error(transparent)]
Session(#[from] session::Error),
/// A Db Connection Error
#[error("Db connection error: {0}")]
ConnectionError(#[from] PostgresConnectionError),
/// A Db Query Error
#[error("Db error: {0}")]
PostgresError(#[from] tokio_postgres::Error),
/// A variant to map `serde_json` errors.
#[error("JSON serialization/deserialization error: {0}")]
SerdeJson(#[from] serde_json::Error),
/// A variant to map `rmp_serde` encode errors.
#[error("Rust MsgPack encode error: {0}")]
RmpSerdeEncode(#[from] rmp_serde::encode::Error),
/// A variant to map `rmp_serde` decode errors.
#[error("Rust MsgPack decode error: {0}")]
RmpSerdeDecode(#[from] rmp_serde::decode::Error),
}
#[derive(Clone, Debug)]
pub struct PostgresStore {
pool: DBPool,
schema_name: String,
table_name: String,
}
impl PostgresStore {
pub async fn new(pool: &DBPool) -> Self {
Self {
pool: pool.clone(),
schema_name: "tower_sessions".to_string(),
table_name: "session".to_string(),
}
}
pub async fn migrate(&self) -> Result<(), PostgresStoreError> {
let create_schema_query = format!(
r#"create schema if not exists "{schema_name}""#,
schema_name = self.schema_name,
);
let mut conn = self
.pool
.get()
.await
.map_err(PostgresStoreError::ConnectionError)?;
let tx = conn.transaction().await?;
if let Err(err) = tx.execute(&create_schema_query, &[]).await {
if !err
.to_string()
.contains("duplicate key value violates unique constraint")
{
return Err(PostgresStoreError::PostgresError(err));
}
return Ok(());
}
let create_table_query = format!(
r#"
create table if not exists "{schema_name}"."{table_name}"
(
id text primary key not null,
data bytea not null,
expiry_date timestamptz not null
)
"#,
schema_name = self.schema_name,
table_name = self.table_name
);
let _ = tx.execute(&create_table_query, &[]).await;
let _ = tx.commit().await;
Ok(())
}
}
#[async_trait]
impl ExpiredDeletion for PostgresStore {
async fn delete_expired(&self) -> Result<(), Self::Error> {
let query = format!(
r#"
delete from "{schema_name}"."{table_name}"
where expiry_date < (now() at time zone 'utc')
"#,
schema_name = self.schema_name,
table_name = self.table_name
);
let conn = self
.pool
.get()
.await
.map_err(PostgresStoreError::ConnectionError)?;
let _ = conn.execute(&query, &[]).await;
Ok(())
}
}
#[async_trait]
impl SessionStore for PostgresStore {
type Error = PostgresStoreError;
async fn save(&self, session: &Session) -> Result<(), Self::Error> {
let query = format!(
r#"
insert into "{schema_name}"."{table_name}" (id, data, expiry_date)
values ($1, $2, $3)
on conflict (id) do update
set
data = excluded.data,
expiry_date = excluded.expiry_date
"#,
schema_name = self.schema_name,
table_name = self.table_name
);
let data = rmp_serde::to_vec(&session)?;
let conn = self
.pool
.get()
.await
.map_err(PostgresStoreError::ConnectionError)?;
let _ = conn
.execute(
&query,
&[&session.id().to_string(), &data, &session.expiry_date()],
)
.await;
Ok(())
}
async fn load(&self, session_id: &Id) -> Result<Option<Session>, Self::Error> {
let query = format!(
r#"
select data from "{schema_name}"."{table_name}"
where id = $1 and expiry_date > $2
"#,
schema_name = self.schema_name,
table_name = self.table_name
);
let cur_date = OffsetDateTime::now_utc();
let conn = self
.pool
.get()
.await
.map_err(PostgresStoreError::ConnectionError)?;
let rows = conn
.query(&query, &[&session_id.to_string(), &cur_date])
.await?;
if let Some(row) = rows.get(0) {
let data: Vec<u8> = row.get("data");
let session: Session = rmp_serde::from_slice(&data)?;
return Ok(Some(session));
}
Ok(None)
}
async fn delete(&self, session_id: &Id) -> Result<(), Self::Error> {
let query = format!(
r#"delete from "{schema_name}"."{table_name}" where id = $1"#,
schema_name = self.schema_name,
table_name = self.table_name
);
let conn = self
.pool
.get()
.await
.map_err(PostgresStoreError::ConnectionError)?;
let _ = conn.execute(&query, &[&session_id.to_string()]).await;
Ok(())
}
}
Hello,
I was following #60 a few weeks ago and was glad to see it already implemented, thanks for the quick work!
On trying the new version of tower-sessions in my server, I've noticed the max-age attribute is still set, e.g. this code:
let session_store = MokaStore::new(None);
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnInactivity(Duration::seconds(10))),
);
yields this reply on login:
set-cookie: tower.sid=79af9808-fb5d-457f-bc11-c096f6bc24ec; HttpOnly; SameSite=Strict; Path=/; Max-Age=9
So that even spamming requests every few seconds that each send the cookie, after 10 seconds the browser will stop sending the cookie and you'll stop being connected at this point from login.
I believe that would only work if we keep sending a new set-cookie with the same value and a new max-age on every single request? (my server uses session.get on every request, but not session.insert as I see in examples -- perhaps the current code would reset the cookie if insert was made?)
But I also just don't see the point of setting a max age here: it's something the server should enforce; if an old cookie is sent we just shouldn't find the session so it should behave as if nothing had been sent.
... Speaking of which I tried with curl:
curl -H 'Cookie: tower.sid=79af9808-fb5d-457f-bc11-c096f6bc24ec' https://myserver
and the server still happily found the session despite the time being long overdue; I thought the point of MokaStore vs MemoryStore was that Moka would properly clean up expired sessions ?
Should I try something else e.g. in-memory sqlite? (I picked moka because the binary size was smaller with it than sqlite as this is for an embedded product and I'd very much like to keep the server small...)
Ah, I see tower-sessions 0.3.3 did properly remove old entries, so this looks like a new bug.
(While I'm here, I think it'd be great API-wise to allow "seconds from now" as a possibility, e.g.
pub enum Expiry {
OnSessionEnd, // browser session end
OnInactivity(Duration), // duration from last request
AfterDuration(Duration), // duration from cookie being created
AtDateTime(OffsetDateTime), // absolute date
I don't quite see how to use AtDateTime because the session manager layer is initialized at server startup, so it doesn't seem practical to update on every request...? Or is that something else I missed? Happy to fork this part of the topic to a discussion or something else; let's focus on "inactivity" not being inactivity in this issue if this is controvertial...)
Tower-sessions offers various sessions storages backed by relational databases. It only supports sqlx
based connections for these stores. It would be great to allow other database connection types like that one provided by diesel/diesel-async as well, as users might prefer these for various reasons.
Hi Max, thanks for the great library, I am trying to migrate from axum-sessions to tower session but running into a bit of hiccup,
When using tower-sessions with Axum's Router, I've found that session cookies are not being set as expected when using nest to define a nested router. Cookies work fine without nesting, but seem to fail when used in a nested scope. Even when I try to use it inside the nested router directly it doesn't set the cookie for some reason.
Session cookies should be set and retrievable, regardless of whether the route is nested.
Session cookies are not being set when using nested routes.
use std::net::SocketAddr;
use axum::{
error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
use tower_sessions::{
cookie::time::Duration, fred::prelude::*, RedisStore, Session, SessionManagerLayer,
};
const COUNTER_KEY: &str = "counter";
#[derive(Clone)]
struct AppState {
session_store: RedisStore,
}
#[derive(Serialize, Deserialize, Default)]
struct Counter(usize);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = RedisClient::default();
let redis_conn = client.connect();
client.wait_for_connect().await?;
let session_store = RedisStore::new(client);
let state = AppState {
session_store: session_store.clone(),
};
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_max_age(Duration::seconds(10)),
);
// Working fine without nest??
// let app = Router::new()
// .route("/", get(handler))
// .layer(session_service)
// .with_state(state.clone());
// Not working why cookie is not being set??
let app = Router::new()
.nest("/api", another_route(state.clone()))
.layer(session_service)
.with_state(state.clone());
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await?;
redis_conn.await??;
Ok(())
}
async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session
.get(COUNTER_KEY)
.expect("Could not deserialize.")
.unwrap_or_default();
session
.insert(COUNTER_KEY, counter.0 + 1)
.expect("Could not serialize.");
format!("Current count: {}", counter.0)
}
// fn another_route(app_state: AppState) -> Router<AppState> {
// let session_service = ServiceBuilder::new()
// .layer(HandleErrorLayer::new(|_: BoxError| async {
// StatusCode::BAD_REQUEST
// }))
// .layer(
// SessionManagerLayer::new(app_state.session_store)
// .with_secure(false)
// .with_max_age(Duration::seconds(10)),
// );
// Router::new()
// .route("/test", get(handler_test))
// .layer(session_service)
// }
fn another_route(app_state: AppState) -> Router<AppState> {
Router::new().route("/test", get(handler_test))
}
async fn handler_test() -> impl IntoResponse {
format!("Hello world!")
}
rustc 1.71.0 (8ede3aae2 2023-07-12)
version: { version = "0.3.3", features = ["redis-store"] }
0.6.20
Linux linux 6.5.8-zen1-1-zen #1 ZEN SMP PREEMPT_DYNAMIC Thu, 19 Oct 2023 22:51:49 +0000 x86_64 GNU/Linux
I am not sure why it's happening I tried debugging it and I can see that the cookie config is being set and all but it's not being sent.
I tested it with Insomnia and the browser itself in this case (Firefox).
When trying to use a RedisStore inside a CachingSessionStore, it fails, because RedisStore
doesn't implement debug.
let config = RedisConfig::from_url(&CONFIG.redis_url)?;
let perf = PerformanceConfig::default();
let policy = ReconnectPolicy::default();
let client = RedisClient::new(config, Some(perf), Some(policy));
let redis_conn = client.connect();
client.wait_for_connect().await?;
let moka_store = MokaStore::new(Some(2000));
let redis_store = RedisStore::new(client);
let caching_store = CachingSessionStore::new(moka_store, redis_store);
let session_layer = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(caching_store)
.with_secure(true)
.with_max_age(time::Duration::seconds(10)),
);
error[E0277]: `RedisStore` doesn't implement `std::fmt::Debug`
--> apps/appealsy-api/src/lib.rs:49:38
|
49 | SessionManagerLayer::new(caching_store)
| ------------------------ ^^^^^^^^^^^^^ `RedisStore` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
| |
| required by a bound introduced by this call
|
= help: the trait `std::fmt::Debug` is not implemented for `RedisStore`
= help: the trait `SessionStore` is implemented for `CachingSessionStore<Cache, Store>`
= note: required for `CachingSessionStore<MokaStore, RedisStore>` to implement `SessionStore`
note: required by a bound in `SessionManagerLayer::<Store>::new`
--> /home/carter/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tower-sessions-0.2.1/src/service.rs:255:13
|
255 | impl<Store: SessionStore> SessionManagerLayer<Store> {
| ^^^^^^^^^^^^ required by this bound in `SessionManagerLayer::<Store>::new`
...
267 | pub fn new(session_store: Store) -> Self {
| --- required by a bound in this associated function
error[E0599]: the method `with_secure` exists for struct `SessionManagerLayer<CachingSessionStore<MokaStore, RedisStore>>`, but its trait bounds were not satisfied
--> apps/appealsy-api/src/lib.rs:50:18
|
49 | / SessionManagerLayer::new(caching_store)
50 | | .with_secure(true)
| | -^^^^^^^^^^^ method cannot be called due to unsatisfied trait bounds
| |_________________|
|
|
::: /home/carter/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tower-sessions-0.2.1/src/session_store.rs:59:1
|
59 | pub struct CachingSessionStore<Cache: SessionStore, Store: SessionStore> {
| ------------------------------------------------------------------------ doesn't satisfy `_: SessionStore`
|
= note: the following trait bounds were not satisfied:
`CachingSessionStore<MokaStore, RedisStore>: SessionStore`
error[E0277]: `RedisStore` doesn't implement `std::fmt::Debug`
--> apps/appealsy-api/src/lib.rs:49:13
|
49 | SessionManagerLayer::new(caching_store)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `RedisStore` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`
|
= help: the trait `std::fmt::Debug` is not implemented for `RedisStore`
= help: the trait `SessionStore` is implemented for `CachingSessionStore<Cache, Store>`
= note: required for `CachingSessionStore<MokaStore, RedisStore>` to implement `SessionStore`
note: required by a bound in `SessionManagerLayer`
--> /home/carter/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tower-sessions-0.2.1/src/service.rs:154:39
|
154 | pub struct SessionManagerLayer<Store: SessionStore> {
| ^^^^^^^^^^^^ required by this bound in `SessionManagerLayer`
Hi, I have an Axum app that I just updated from 0.6 to 0.7, and also switched from axum_sessions to this, since that was recommended. I believe I'm doing the same thing as your example and getting a weird compiler error on the line that adds the SessionManagerLayer
to the Router
.
error[E0277]: the trait bound `Infallible: From<Box<(dyn StdError + std::marker::Send + Sync + 'static)>>` is not satisfied
--> src/main.rs:61:13
|
60 | .layer(
| ----- required by a bound introduced by this call
61 | / SessionManagerLayer::new(session_store)
62 | | .with_secure(false)
| |___________________________________^ the trait `From<Box<(dyn StdError + std::marker::Send + Sync + 'static)>>` is not implemented for `Infallible`
|
= help: the trait `From<!>` is implemented for `Infallible`
= note: required for `Box<(dyn StdError + std::marker::Send + Sync + 'static)>` to implement `Into<Infallible>`
note: required by a bound in `Router::<S>::layer`
--> /Users/micahyoder/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.7.1/src/routing/mod.rs:238:50
|
233 | pub fn layer<L>(self, layer: L) -> Router<S>
| ----- required by a bound in this associated function
...
238 | <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
| ^^^^^^^^^^^^^^^^ required by this bound in `Router::<S>::layer`
The session_store is being set up like this:
let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false);
Am I doing something wrong or is this a bug? Thanks.
0.12.2
Linux hostname 5.10.0-21-amd64 #1 SMP Debian 5.10.162-1 (2023-01-21) x86_64 GNU/Linux
Calling session.save()
correctly creates new sessions in the backing store but the cookie is not set.
I'm implementing a login handler.
My sessions are stored in a database table (using tower-sessions-rorm-store
) and my table stores additional data besides the id
, data
and expiry_date
stored in your Record
type.
My handler code basically checks the credentials, calls session.save()
(which creates the session in the DB) and then uses the session's id to set some additional information on my DB column directly, but leaves the session's data hashmap unchanged.
I expected to see this happen: A new session is created in the DB and the cookie is set.
Instead, this happened: A new session is created in the DB but the cookie is not set.
I "solved" the problem for my project my setting an unused value:
// [...] check credentials
// This is required to mark the session as modified because the cookie won't be set otherwise
session
.insert_value("mark_dirty", Default::default())
.await?;
session.remove_value("mark_dirty").await?;
// We have to call save manually as the id is only populated after creating the session
session.save().await?;
// [...] modify inserted session using session.id()
I am not sure what the exact cause is, but it is very easy to get a deadlock.
It does not matter which store I use.
I tried to diagnose the issue, tokio-console, but that didn't help.
Which information do you need for better investigating this issue?
Hi currently there you can only create cookies with the value http_true: true
.
Can we add a with_http_only
fn to the SessionManagerLayer for overriding that behaviour?
Thanks for making this!
With the latest version (0.8
) it seems custom session store implementations are not possible because the Result
and Error
types are not exported. This fixes it for me:
modified tower-sessions-core/src/lib.rs
@@ -4,7 +4,7 @@ pub use tower_cookies::cookie;
pub use self::{
service::{SessionManager, SessionManagerLayer},
session::{Expiry, Session},
- session_store::{CachingSessionStore, ExpiredDeletion, SessionStore},
+ session_store::{CachingSessionStore, ExpiredDeletion, SessionStore, Result, Error},
};
#[cfg(feature = "axum-core")]
Additionally, the latest changes have made it more challenging to test my custom implementation because I was previously relying on the Serialize
instance of Session
to check for equality in tests, but now the serialization is done through Record
, and Session
doesn't have a public API to access the Record
. Maybe we could add a Serialize
implementation to Session
that just delegates to the Record
serializer?
I've made a SurrealDB store, you can see what I'm trying to do in my tests here: https://github.com/rynoV/tower-sessions-surrealdb-store/blob/master/src/lib.rs. Right now I've just set Session::get_record
to public (in my local copy) to facilitate my tests.
Version 0.10.3 added lifetime parameter to SessionManager - https://docs.rs/tower-sessions/latest/tower_sessions/service/struct.SessionManager.html
As per https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-parameter-no-default this is a Major change and yet only a patch version was released
This e.g. breaks builds of axum-login
$ cargo tree | grep tower-sessions
โ โโโ tower-sessions v0.10.3
โ โ โโโ tower-sessions-core v0.10.3
โ โ โโโ tower-sessions-memory-store v0.10.3
โ โ โ โโโ tower-sessions-core v0.10.3 (*)
$ uname -a
Linux qdell-7080 6.5.0-14-generic #14~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Nov 20 18:15:30 UTC 2 x86_64 GNU/Linux
As specified, failed in a project depending on axum-login-0.13
I have a problem with axum-login crate which uses tower-sessions.
For example:
auth.example.localhost - sets a cookie "id" for a domain ".example.localhost"
Then on logout session.flush().await?;
is called but response
Set-Cookie: id=; Max-Age=0; Expires=Mon, 06 Feb 2023 09:20:24 GMT
does not touch a cookie. (I think because there is no "domain" param)
So a cookie is not removed from browser causing warnings:
tower_sessions_core::session: possibly suspicious activity: record not found in store
or maybe I did something wrong. But it looks strange to me.
Considering that caching implementations of SessionStore
exist, why does SessionManager
also cache sessions by default? Furthermore, it does so without setting any limit on the number of cached sessions, and only deletes them from the cache once the sessions are specifically marked as deleted or fully emptied out.
tower-sessions v0.11.0
tower-sessions-core v0.11.0
tower-sessions-memory-store v0.11.0
tower-sessions-sqlx-store v0.11.0
Windows 10 64-bit
When using set_expiry
on a session the new expiry date is not saved in the session store. This seems to be because set_expiry
updates the field expiry
, but not the record; only the record is sent to the session store.
I tried this code:
let expired_at = OffsetDateTime::now_utc().saturating_add(Duration::days(60));
session.set_expiry(Some(Expiry::AtDateTime(expired_at)));
I expected to see this happen:
The expiry date in the database should be updated, and the session should last 60 days.
Instead, this happened:
The expiry date in the database was updated according to the previous expiry (OnInactivity) and the session expired.
The expiry time of the cookie was correctly updated, this resulted in a warning on the server:
WARN [tower_sessions_core::session] possibly suspicious activity: record not found in store
let session_store = RedisStore::new(pool);
this line gives err
because it needs a client not a pool..
while I prefer pool not client .. as the example suggests .. but it is just gives err
|
26 | let session_store = RedisStore::new(pool);
| --------------- ^^^^ expected `RedisClient`, found `RedisPool`
| |
| arguments to this function are incorrect
tower-sessions = "=0.10.4"
Linux a1fc3b8591b7 5.15.0-97-generic #107-Ubuntu SMP Wed Feb 7 13:26:48 UTC 2024 x86_64 GNU/Linux
tower-sessions
The following code compiles with tower-sessions 0.10.2 but not with 0.10.4. It might be related to #160.
use tower_sessions::{MemoryStore, SessionManagerLayer};
fn create_layer(cookie_name: String) -> SessionManagerLayer<MemoryStore> {
let session_store = MemoryStore::default();
SessionManagerLayer::new(session_store).with_name(&cookie_name)
}
fn main() {
create_layer("cookie_foo".to_string());
}
error[E0597]: `cookie_name` does not live long enough
--> src/main.rs:5:55
|
3 | fn create_layer(cookie_name: String) -> SessionManagerLayer<MemoryStore> {
| ----------- binding `cookie_name` declared here
4 | let session_store = MemoryStore::default();
5 | SessionManagerLayer::new(session_store).with_name(&cookie_name)
| --------------------------------------------------^^^^^^^^^^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `cookie_name` is borrowed for `'static`
6 | }
| - `cookie_name` dropped here while still borrowed
I'd like to explore breaking this crate up into multiple crates with a core crate, providing the session, the store interface, tower service, and axum extractor alongside crates for specific store implementations.
Some users prefer using NoSQL, one of the most popular one is MongoDB. Is there any chance this one will be implemented?
It seems like it's probably better practice to use a pool instead of a single client.
Hello,
I try to use a SessionManagerLayer
like this:
let session_layer = SessionManagerLayer::new(session_store)
.with_name("my_session")
.with_secure(false)
.with_expiry(Expiry::OnSessionEnd);
Unfortunately, the the cookie expires in 2 weeks and is not a cookie of session. I have the same result with:
let session_layer = SessionManagerLayer::new(session_store)
.with_name("my_session")
.with_secure(false);
Did I miss something?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.