Giter Club home page Giter Club logo

axum-sessions's People

Contributors

dependabot[bot] avatar erikwegner avatar extrawurst avatar jmmv avatar leeh-peter avatar maxcountryman avatar ptrskay3 avatar quasiuslikecautious avatar spanfile avatar sunhuachuang avatar tekul 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

axum-sessions's Issues

Deadlock when using WritableSession in middleware layer

Hi,
I am using axum-sessions inside a middleware layer that is checking if the user is logged in and short-circuits the router with a StatusCode::UNAUTHORIZED response if they are not.

If I then try to use a WritableSession in a route that is "behind" this middleware layer I get a deadlock that I am guessing is caused by the internal RWLock that is being acquired while the middleware layer is still holding a reference to the ReadableSession.

For the time being I am going to work-around this issue by bypassing the middleware when I need a WritableSession but is there a way to release the ReadableSession from the middleware after I am done using it so that I can avoid this issue?

Here's a minimal POC:

Cargo.toml

[package]
name = "axum-sessions-deadlock"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.6"
axum-sessions = "0.5"
hyper = "0.14"
rand = "0.8"
tokio = { version = "1", features = ["full"] }
toml = "0.7"
tower-http = "0.4"

main.rs

use axum::http::Request;
use axum::middleware::Next;
use axum::response::Response;
use axum::{routing::get, Router};
use axum_sessions::extractors::{ReadableSession, WritableSession};
use axum_sessions::{async_session::MemoryStore, SessionLayer};
use hyper::StatusCode;
use rand::RngCore;

#[tokio::main]
async fn main() {
    let store = MemoryStore::new();
    let mut secret = [0u8; 128];
    rand::thread_rng().fill_bytes(&mut secret);
    let session_layer = SessionLayer::new(store, &secret).with_secure(false);

    let app = Router::new()
        .route("/", get(deadlock))
        .layer(axum::middleware::from_fn(auth_middleware))
        .layer(session_layer);

    eprintln!("Listening on 127.0.0.1:3000");

    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

pub async fn auth_middleware<T>(
    session: ReadableSession,
    request: Request<T>,
    next: Next<T>,
) -> Result<Response, StatusCode> {
    // e.g: check if user is logged in
    // let login: Option<String> = session.get("login");
    // match login {
    //     None => Err(StatusCode::UNAUTHORIZED),
    //     Some(_) => Ok(next.run(request).await),
    // }

    // assume logged-in
    Ok(next.run(request).await)
}

async fn deadlock(mut session: WritableSession) -> StatusCode {
    // e.g: session.destroy();
    StatusCode::NO_CONTENT
}

Thanks

How to destroy then insert in the same route?

Hi, I tried calling session.destroy() in my route handler and then session.insert() because I want this particular route handler to be able to start with a clean slate whenever it is called. I found that the insert call failed. Is it possible to destroy() then insert() in the same handler one line after the other?

Memory session is not persisted (on RPi Arm)

Hi, I have an issue with the memory session.
It works when I am testing on Ubuntu at x64, however it does not on Arm Raspberry.

I am using a simple password only authentication to protect the view with the MemoryStore:

let store = MemoryStore::new();
let secret = rand::thread_rng().gen::<[u8; 128]>();
let session_layer = SessionLayer::new(store, &secret);

(i guess it is straight from the examples)

Then in my login endpoint I simply do:

session.insert("auth", true)
        .expect("Session write error!");

And the expect is not called - so the session mechanism works. (which I confirmed by printing it to the console at this point).

But, when I try to read the session in the protected endpoint I get a None value:

    if !session.get("auth").unwrap_or(false) {
        return Redirect::to("/login").into_response();
    }

(on Raspbian, on Ubuntu it is fine - I get the proper session state)

I am not sure if the issue is not coming from the async-session crate - but have no idea at the moment how to figure it out.
I wonder what are the differences between those platforms that can cause that. I mean probably on the Pi the memory state is dropped or not properly shared between the async calls (?)

"Session handle still has owners"

I was trying to use axum-sessions within the leptos framework and I encountered an error where leptos seemed like it would not drop AuthContext. I reported it to leptos and it seems like it is an issue with your middleware:

let session = RwLock::into_inner(
Arc::try_unwrap(session_handle).expect("Session handle still has owners."),
);

Why does this type implement Clone and then panic if it is cloned and used? Why is this not documented? Surely this edge case can be worked around, or at least pushed to a compile-time error?

If you would like to see the issue, this project errors with this reproducibly here

Cannot retrieve records in handlers

Hi, I am able to insert a record into session in one handler but unable to retrieve it in another. Could you:

  1. Put some docs with a how to example
  2. Let me know how to set the session and store as a layer in Axum
  3. How to use Redis as the session store

Since docs are not present in this crate currently, I proceeded with docs from async-sessions and got the following errors:

90 | let cookie_value = store.store_session(session).await;
| ^^^^^^^^^^^^^ method not found in Result<RedisSessionStore, redis::types::RedisError>

error[E0599]: no method named load_session found for enum Result in the current scope
--> src/main.rs:91:29
|
91 | let mut session = store.load_session("test-cookie".parse().unwrap()).await.unwrap();
| ^^^^^^^^^^^^ method not found in Result<RedisSessionStore, redis::types::RedisError>

From the following code:

async fn do_something(Extension(session): Extension<Session>) -> Json<Value> {
    let store = RedisSessionStore::new("redis://127.0.0.1/");
    let cookie_value = store.store_session(session).await;
    let mut session = store.load_session("test-cookie".parse().unwrap()).await.unwrap();

    let user_id: String = session.get::<String>("user_id").unwrap();
    Json(json!({ "data": user_id }))
}

Appreciate any help on this front.

Type-safe `Session` handles

Since Axum 6.0 it's possible to use State instead of Extension to avoid runtime errors (see tokio-rs/axum#1155). I was wondering if the same could possibly be implemented for ReadableSession/WritableSession.

WritableSession Load Forever

i am having some issue with axum-sessions. WritableSession is just loading forever. But same Wrirable session works in my 3 handler function but for weird reason it just does not in one of my handler.

Does not work handler

pub async fn store_admin_user_handler(  
   mut session: WritableSession,  
   app_state: State<Arc<AppState>>,  
   Form(payload): Form<StoreAdminUserRequest>,  
) -> Result<impl IntoResponse, impl IntoResponse> { 
      Ok(Json(json!({})).into_response())  
 }

Below handler it works perfectly fine.

 pub async fn post_admin_login_handler(
     mut session: WritableSession,
     app_state: State<Arc<AppState>>,
     Form(payload): Form<LoginAdminUserRequest>,
 ) -> Result<impl IntoResponse, impl IntoResponse> { 
    Ok(Json(json!({})).into_response())  
  }

Deadlock when using both read and write in the same handler

Having read an issue in axum-login I realized that it's really easy to get a deadlock with the current API of this crate. Since ReadableSession and WritableSession both using the same underlying resource behind an RwLock.

A simple axum handler to reproduce:

async fn deadlock(mut _write_session: WritableSession, _read_session: ReadableSession) {}

and this can be solved if you release the lock guard

async fn deadlock(mut _write_session: WritableSession, read_session: ReadableSession) {
    // do the read here..
    drop(read_session);
    // write operations get to run here..
}

This issue becomes especially awkward and more subtle when this becomes an implicit bound, just like here. AuthContext needs a write access, but also there's a session read in the same handler. The developer must be familiar with the implementation details of these structs to know to release the read guard.

I don't really know how to solve this with the current API, but I'm experimenting..

Handle multiple cookie values sent in a request

First of all, thank you for making this lib.

I noticed that when multiple cookies sent in a request, it's not parsed correctly. For example with this cookie

first=asdf; axum.sid=1234

currently the name() of the cookie is first, and the value is asdf; axum.sid=1234, thus it fails to identify the session_layer.cookie_name is actually present.

The relevant part of the code is:

 let cookie_value = request
            .headers()
            .get(COOKIE)
            .and_then(|cookie| Cookie::parse_encoded(cookie.to_str().unwrap()).ok())
            .filter(|cookie| cookie.name() == session_layer.cookie_name)
            .and_then(|cookie| session_layer.verify_signature(cookie.value()).ok());

I think something like this should work (please check, I haven't tested this very thoroughly).

        let cookie_values = request
            .headers()
            .get(COOKIE)
            .map(|cookies| cookies.to_str());

        let cookie_value = if let Some(Ok(cookies)) = cookie_values {
            cookies
                .split(";")
                .map(|cookie| cookie.trim())
                .filter_map(|cookie| Cookie::parse_encoded(cookie).ok())
                .filter(|cookie| cookie.name() == session_layer.cookie_name)
                .find_map(|cookie| self.layer.verify_signature(cookie.value()).ok())
        } else {
            None
        };

Let me know what do you think about it.

Consider switching to `ring` for consistency with `axum-login`

I recently went through the exercise of putting together a crate for session-based user authentication and in doing that decided to use ring for HMAC. (I did this without thinking too much about it, since I've previously used ring in other projects.)

However, axum-extra (used for axum-sessions) uses the cookie crate which in turn uses the hmac crate. Previously, it looks like cookie used ring but it's unclear why the decision was made to move away from ring.

I should point out that this is not my area of expertise. I've chosen ring previously based on the apparent consensus that it's one of the premiere cryptographic crates with the primary difference between ring and the RustCrypto family being that parts of ring are not implemented in Rust (e.g. ring derives from BoringSSL).

How to persist postgres sessions between server restarts or load balanced servers?

I've almost got things working perfectly, I'm just missing one detail that I can't put my finger on. Here's what I've got:

  • axum
  • axum-session
  • async-sqlx-session

And here's a pared down version of main.rs

#[tokio::main]
async fn main() {
    let db_url = env::var("DATABASE_URL").unwrap();

    // session cookie ------ start
    let store = PostgresSessionStore::new(&db_url)
        .await
        .map_err(|e| {
            eprintln!("Database error: {:?}", e);
            RestAPIError::InternalServerError
        })
        .expect("Could not create session store.");
    store
        .migrate()
        .await
        .expect("Could not migrate session store.");
    store.spawn_cleanup_task(Duration::from_secs(60 * 60));
    // let mut session = Session::new();
    //
    // let cookie_value = store.store_session(session).await.unwrap().unwrap();
    // let session = store.load_session(cookie_value).await.unwrap();
    // println!("main session: {:?}", session);

    let secret = random::<[u8; 128]>();
    let session_layer = SessionLayer::new(store, &secret)
        .with_secure(true)
        .with_same_site_policy(SameSite::Strict)
        .with_persistence_policy(PersistencePolicy::ChangedOnly);
    // session cookie ------ end

    let routes_all = Router::new()
        .nest("/api/campaign", routes_campaign::routes())
        .layer(session_layer);

    // start server -------- start
    // Default IP as a fallback. This is what it should be for running on my local dev machine without Docker
    let default_ip = "127.0.0.1";

    let host: IpAddr = env::var("SERVER_LISTEN_HOST")
        .unwrap_or(default_ip.to_string()) // Use the default IP if the env variable is missing
        .parse() // Try parsing the string into an IpAddr
        .expect("Failed to parse host (listen on) IP address"); // If parsing fails, this will panic. You can handle this more gracefully if needed.

    let addr = SocketAddr::from((host, 8080));
    println!("Listening on {addr}\n");
    axum::Server::bind(&addr)
        .serve(routes_all.into_make_service())
        .await
        .unwrap();

    // start server -------- end
}

What I am finding is that the database session tables are getting updated correctly and the session persists between browser page reloads but if I stop my server and restart it, a new session is created and the old one is orphaned in the database. So, on start, the existing session is not being rehydrated. I think this must have something to do with the way the session is instantiated and is not using the sid cookie passed by the browser but I'm trying to work out what I need to modify.

Thanks for looking.

Implementing `MemoryStore`

Hey,

I'm trying to write a custom MemoryStore that works with a sqlx postgres database to store sessions for axum-login.
I've got the following SQL, which has space for all the non-serde-skipped parts of Session:

CREATE TABLE auth_sessions (
    id TEXT PRIMARY KEY NOT NULL,
    expiry TIMESTAMP
);

CREATE TABLE auth_session_data (
    k TEXT NOT NULL,
    v TEXT NOT NULL,

    session_id TEXT NOT NULL,
    CONSTRAINT fk_session_id
        FOREIGN KEY (session_id)
        REFERENCES auth_sessions(id)
        ON DELETE CASCADE
)

Unfortunately, I can't work out a way to get the session contents to/from SQL, as I can't seem to find a constructor for making a Session, or getting all of the data - I can get values from keys but if I don't know what keys are being used (and would rather not have to hardcode a list from axum-login), and I can make a new cookie and add data but I can't do things like set the ID.

How to retrieve a readable session in middleware?

The example on docs.rs cites the following code:

async fn handle(request: Request<Body>) -> Result<Response<Body>, Infallible> {
    let session_handle = request.extensions().get::<SessionHandle>().unwrap();
    let session = session_handle.read().await;
    // Use the session as you'd like.

    Ok(Response::new(Body::empty()))
}

This is what I have:

pub async fn flash<B>(mut req: Request<B>, next: Next<B>) -> Result<impl IntoResponse> {
    let session_handle = req.extensions().get::<SessionHandle>().unwrap();
    let session = session_handle.read().await;

    let payload = session.get::<bool>("signed_in").unwrap_or(false);

    req.extensions_mut().insert(payload);
    Ok(next.run(req).await)
}

image

However, I want to embed state in the Request so that the next handler knows whether the user is logged in or not. I'm using Askama for templating in case this is relevant.

Regenerating session from request handler doesn't work

Calling regenerate from a request handler does not refresh the cookie for the client. Having a reliable way to refresh cookie values is essential in most applications: secure session based authentication systems rotate the cookie value on any privilege level change to prevent session fixation attacks.

This is happening because we insert the cloned session to the request handler as an extension

request.extensions_mut().insert(session.clone());
let mut response = inner.call(request).await?;

and the original session in the middleware is still holding onto the old cookie_value

} else if session_layer.save_unchanged || session.data_changed() {
match session_layer.store.store_session(session).await {

so it won't see the regenerate call from the request handler.

I have a workaround for this, but it's by no means pretty, and requires an extra round-trip to the session store.

Basically I defined an extension trait for async_session::Session to add two methods:

pub trait SessionExt {
    const REGENERATION_MARK_KEY: &'static str;

    fn mark_for_regenerate(&mut self);
    fn should_regenerate(&mut self) -> bool;
}

impl SessionExt for async_session::Session {
    const REGENERATION_MARK_KEY: &'static str = "__regenerate_key";

    fn mark_for_regenerate(&mut self) {
        self.insert(Self::REGENERATION_MARK_KEY, true)
            .expect("bool is serializable");
    }

    fn should_regenerate(&mut self) -> bool {
        let previously_changed = self.data_changed();
        let regenerate = self.get(Self::REGENERATION_MARK_KEY).unwrap_or_default();

        self.remove(Self::REGENERATION_MARK_KEY);
        if !previously_changed {
            self.reset_data_changed();
        }

        regenerate
    }
}

In the middleware we could insert

     if session.should_regenerate() {
            session.regenerate();
         }

and in request handlers consumers should call mark_for_regenerate instead of regenerate.
However, this solution is definitely a little ambiguous/deceptive for users..

How to make one-shot session?

Thanks for creating axum-sessions and axum-login. I have a question: How to create one-shot session, that session will be destroyed after user closes the browser?

I want to implement login mode where user will be logged out if he/she closes browser.

Session not saving when returning a redirect response

Basically the issue is that I have an endpoint (lets call it auth endpoint) that fetches some data from a 3rd party application and based on the response from that app if it's successful it inserts key signed_in into a session, then the client is redirected to another endpoint (lets call it data endpoint). and from inside that other endpoint when i try to read the session it says that the key signed_in is None, this is the issue. but when the auth endpoint sends a response back without redirecting.. it saves the session successfully and when i navigate to the data endpoint it reads the session correctly.

Excuse me for my rusty English and my terrible explanation but I hope you get the idea.

Edit:
Here is a better explanation

the issue

fn auth_endpoint(mut session: WriteableSession) {
    do_some_logic();
    session.insert("signed_in", true).unwrap();
    return Redirect::to("/data-endpoint");
}

fn data_endpoint(session: ReadableSession) {
    let signed_in = session.get::<bool>("signed_in").unwrap_or(false);
    // signed_in is None (false)
}

The walk-around

fn auth_endpoint(mut session: WriteableSession) {
    do_some_logic();
    session.insert("signed_in", true).unwrap();
    return "OK!"
}
// then when a user manually navigates to this /data-endpoint
fn data_endpoint(session: ReadableSession) {
    let signed_in = session.get::<bool>("signed_in").unwrap_or(false);
    // signed_in is true
}

Is there a way i can fix this or am i doing something wrong?

Bug: data present in session, but `.get` returns `None`

Screenshot 2023-07-20 at 4 42 19 PM Screenshot 2023-07-20 at 4 43 07 PM

first screenshot: you can see in the left pane that string is populated with some stringified json, but in the
second screenshot: the result of session.get(key) is None. you can also see the data is available inside the session variable.

it may be (likely) that the serde_json::to_string is failing, but i have no visibility into that due to the .ok() here, which throws out the error.

IMO, Session::get should return Result<Option<T>,Err>, to distinguish between three possible states:

  • the requested key doesn't have any data in the session store: Ok(None)
  • the requested data: Ok(Some(data))
  • the key does have data, but there was an error getting it to you: Err(...)

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.