GithubHelp home page GithubHelp logo

maxcountryman / tower-sessions Goto Github PK

View Code? Open in Web Editor NEW
182.0 4.0 34.0 351 KB

๐Ÿฅ  Sessions as a `tower` and `axum` middleware.

License: MIT License

Rust 100.00%
axum axum-middleware cookies sessions tower

tower-sessions's Introduction

tower-sessions

๐Ÿฅ  Sessions as a `tower` and `axum` middleware.

๐ŸŽจ Overview

This crate provides sessions, key-value pairs associated with a site visitor, as a tower middleware.

It offers:

  • Pluggable Storage Backends: Bring your own backend simply by implementing the SessionStore trait, fully decoupling sessions from their storage.
  • Minimal Overhead: Sessions are only loaded from their backing stores when they're actually used and only in e.g. the handler they're used in. That means this middleware can be installed anywhere in your route graph with minimal overhead.
  • An axum Extractor for Session: Applications built with axum can use Session as an extractor directly in their handlers. This makes using sessions as easy as including Session in your handler.
  • Simple Key-Value Interface: Sessions offer a key-value interface that supports native Rust types. So long as these types are Serialize and can be converted to JSON, it's straightforward to insert, get, and remove any value.
  • Strongly-Typed Sessions: Strong typing guarantees are easy to layer on top of this foundational key-value interface.

This crate's session implementation is inspired by the Django sessions middleware and it provides a transliteration of those semantics.

Session stores

Session data persistence is managed by user-provided types that implement SessionStore. What this means is that applications can and should implement session stores to fit their specific needs.

That said, a number of session store implmentations already exist and may be useful starting points.

Crate Persistent Description
tower-sessions-dynamodb-store Yes DynamoDB session store
tower-sessions-firestore-store Yes Firestore session store
tower-sessions-libsql-store Yes libSQL session store
tower-sessions-mongodb-store Yes MongoDB session store
tower-sessions-moka-store No Moka session store
tower-sessions-redis-store Yes Redis via fred session store
tower-sessions-rorm-store Yes SQLite, Postgres and Mysql session store provided by rorm
tower-sessions-rusqlite-store Yes Rusqlite session store
tower-sessions-sled-store Yes Sled session store
tower-sessions-sqlx-store Yes SQLite, Postgres, and MySQL session stores
tower-sessions-surrealdb-store Yes SurrealDB session store

Have a store to add? Please open a PR adding it.

User session management

To facilitate authentication and authorization, we've built axum-login on top of this crate. Please check it out if you're looking for a generalized auth solution.

๐Ÿ“ฆ Install

To use the crate in your project, add the following to your Cargo.toml file:

[dependencies]
tower-sessions = "0.12.2"

๐Ÿคธ Usage

axum Example

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 {
    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 = 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();
}

You can find this example as well as other example projects in the example directory.

Note

See the crate documentation for more usage information.

๐Ÿฆบ Safety

This crate uses #![forbid(unsafe_code)] to ensure everything is implemented in 100% safe Rust.

๐Ÿ›Ÿ Getting Help

We've put together a number of examples to help get you started. You're also welcome to open a discussion and ask additional questions you might have.

๐Ÿ‘ฏ Contributing

We appreciate all kinds of contributions, thank you!

tower-sessions's People

Contributors

and-reas-se avatar beckend avatar bekriebel avatar cole-h avatar czocher avatar daybowbow-dev avatar dependabot[bot] avatar dominicwrege avatar imbolc avatar justmangoou avatar katze864 avatar martinetd avatar maxcountryman avatar myomikron avatar nul-reference avatar patte avatar plasticbox avatar rynov avatar thallada avatar weiznich avatar wt avatar zatzou avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

tower-sessions's Issues

Reorganize into a workspace of crates

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.

Expiry::AtDateTime behaviour may be surprising

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)

Support auto-prolonging sessions

Problem

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.

Proposition

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).

store session in signed/encrypted cookies

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.

Redis example needs pool not a client

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

Implement an `into_inner` on SessionId

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(),
        )
    }
}

i do not see a postgres table although session works

////////////////// 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 ?

SessionManager infinitely caching sessions in loaded_sessions

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.

Version 0.12.0 not compatible with redis-store 0.11.0

  • I have looked for existing issues (including closed) about this

Bug Report

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,
}

Version

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`:

  • tower-sessions v0.12.0
    • tower-sessions-core v0.12.0
      • tower-sessions-memory-store v0.12.0
      • tower-sessions-core v0.12.0 (*)
    • tower-sessions-core v0.11.1
  • tower-sessions v0.12.0 (*)
  • tower-sessions-redis-store v0.11.0

-->

Platform

MACOS Apple silicone

Crates

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"

Description

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};

Facilitate custom `SessionStore` implementation testing

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.

SessionStore::delete called even if the session was never saved

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...

} else {
// We don't have a session cookie, so let's create a new session.
let session = session_config.new_session();
tracing::debug!("created new session");
session
};

...and here:

// N.B. When a session is empty, it will be deleted. Here the deleted method
// accounts for this check.
if let Some(session_deletion) = session.deleted() {
match session_deletion {
Deletion::Deleted => {
tracing::debug!("deleted");

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.

Performance issues after migrating from axum-sessions

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 (

inner: Arc<Mutex<Inner>>,
)

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

Examples: Unknown type parameter T

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

Support `diesel` as database library

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.

Session Ids can collide

Bug Report

There is nothing preventing id collisions when creating a new session. More discussion here: #169.

Version

main is affected
0.11.1 confirmed

Platform

Presumably all platforms.
Confirmed on Fedora Rawhide Linux as of 2024/03/18.

Crates

tower-sessions
tower-sessions-core

Description

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.

Does .flush() work correctly when a cookie domain is set?

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.

version 0.10.3 broke SemVer guarantees

  • I have looked for existing issues (including closed) about this

Bug Report

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

Version

$ 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 (*)

Platform

$ 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

Crates

Description

As specified, failed in a project depending on axum-login-0.13

Add support for Postgres store with tokio-postgres along with bb8-postgres. Implementation attached.

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(())
    }
}

Divide each store to its own crate

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.

Clearing a session's data does not get persisted to the store

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.

Application Deadlock On v0.4.2

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.

  • PostgresStore after a couple of repaid request
  • with MokaStore and CachingSessionStore after 30 minutes
  • MemoryStore 30 minutes

I tried to diagnose the issue, tokio-console, but that didn't help.

Which information do you need for better investigating this issue?

Support MongoDB

Some users prefer using NoSQL, one of the most popular one is MongoDB. Is there any chance this one will be implemented?

Problems around OnInactivity session expiry

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...)

Session Cookies Not Being Set When Using nest in Axum

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,

Description

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.

Steps to reproduce

  1. Set up an Axum server with tower_sessions for session management.
  2. Create a main route and a nested route using nest.
  3. Add SessionManagerLayer middleware to handle cookies.
  4. Observe that cookies are not being set for the nested route.

Expected Behavior

Session cookies should be set and retrievable, regardless of whether the route is nested.

Actual Behavior

Session cookies are not being set when using nested routes.

Code Sample

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!")
}

Environment:

  • Rust version: rustc 1.71.0 (8ede3aae2 2023-07-12)
  • Tower Sessions: version: { version = "0.3.3", features = ["redis-store"] }
  • Axum version: 0.6.20
  • OS: 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

Additional Context

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).

with_expiry(Expiry::OnSessionEnd) does not seem to work

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?

`RedisStore` doesn't implement `std::fmt::Debug`

When trying to use a RedisStore inside a CachingSessionStore, it fails, because RedisStore doesn't implement debug.

Code

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

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`

When adding an Axum layer: "the trait `From<Box<(dyn StdError + std::marker::Send + Sync + 'static)>>` is not implemented for `Infallible`"

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.

`MemoryStore` Session Expiry

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.

Breaking change in v0.10.4

  • I have looked for existing issues (including closed) about this

Bug Report

Version

tower-sessions = "=0.10.4"

Platform

Linux a1fc3b8591b7 5.15.0-97-generic #107-Ubuntu SMP Wed Feb 7 13:26:48 UTC 2024 x86_64 GNU/Linux

Crates

tower-sessions

Description

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

Max-Age of Cookie not updates on set_expiry()

  • I have looked for existing issues (including closed) about this

Bug Report

Version

tower-sessions v0.11.1
tower-sessions-core v0.11.1
tower-sessions-memory-store v0.11.1

Platform

Windows 10 64-bit

Description

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

`std::option_env!("URL").unwrap()` in tests

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.

Feature Request: Companion Cookie

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.

Inserting forces a sesssion read which could cause the insert to error

  • I have looked for existing issues (including closed) about this

Bug Report

Version

tower-sessions v0.10.4
tower-sessions-core v0.10.4

Platform

Windows 10 64-bit

Description

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

SessionManagerLayer type error after upgrading to 5.0

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!

function or associated item not found in 'Session'

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)

Error Message

guest.zip

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();
}

cannot infer type of the type parameter `T` declared on the struct `HandleErrorLayer`

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

set_expiry() does not work

  • I have looked for existing issues (including closed) about this

Bug Report

Version

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

Platform

Windows 10 64-bit

Description

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

2 Errors when installing with postgres

` ```
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}, _>

Use `core::time::Duration` instead of `time::Duration`

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:

  • You are not forcing the user to use time
  • time::Duration accepts negative durations which wouldn't make sense in an expiry setting

I haven't looked through the complete public API, but I'd suggest unifiying the Durations where it make sense.

Make session loading lazy

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).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.