maxcountryman / axum-sessions Goto Github PK
View Code? Open in Web Editor NEW๐ฅ Cookie-based sessions for Axum via async-session.
License: MIT License
๐ฅ Cookie-based sessions for Axum via async-session.
License: MIT License
The default session cookie name gives away that axum (or this specific middleware) is used:
https://github.com/maxcountryman/axum-sessions/blob/main/src/session.rs#L112
This should be changed to a generic default, like sid
, id
, or ses
.
See also:
where is the docs for connecting to redis or pg ?
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
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?
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 (?)
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:
Lines 346 to 348 in 4c7480a
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
Hi, I am able to insert a record into session in one handler but unable to retrieve it in another. Could you:
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.
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
.
This is a great start: https://keepachangelog.com/en/1.1.0/
GitHub Releases like https://github.com/LeoniePhiline/axum-csrf-sync-pattern/releases also would help dependents know what changed per each release!
Thanks for your consideration.
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())
}
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..
Hi!
Are there any utilities to easily create a ReadableSession
object for testing purposes?
Thanks
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.
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).
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.
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.
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)
}
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.
Add a listener trait to listen to the creation, update and destruction of sessions
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
Lines 279 to 280 in f4b2301
and the original session in the middleware is still holding onto the old cookie_value
Lines 294 to 295 in f4b2301
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..
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.
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?
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:
Ok(None)
Ok(Some(data))
Err(...)
This line in lib.rs needs the async_session
module name removed since MemoryStore
is imported directly. The same example in the README is already correct.
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.