From cb42003dff24024c15aa84c4d13299e6fcaadc5e Mon Sep 17 00:00:00 2001 From: daly4 Date: Tue, 14 Nov 2023 23:00:28 -0700 Subject: [PATCH 1/4] update to tower-sessions --- Cargo.toml | 11 ++-- README.md | 64 ++++++++++-------- examples/cross-site/Cargo.toml | 3 +- examples/cross-site/src/main.rs | 68 +++++++++---------- examples/same-site/Cargo.toml | 3 +- examples/same-site/src/main.rs | 17 ++--- src/lib.rs | 111 ++++++++++++++++++-------------- 7 files changed, 149 insertions(+), 128 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a9b4fad..3d121b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,16 +16,17 @@ edition = "2021" maintenance = { status = "actively-developed" } [dependencies] -axum = "0.6.16" +axum = "0.6.20" axum-core = "0.3.4" -axum-sessions = "0.5.0" -base64 = "0.21.0" +base64 = "0.21.5" rand = "0.8.5" thiserror = "1.0.40" -tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tower = "0.4.13" -tracing = "0.1.37" +tower-cookies = "0.9.0" +tower-sessions = "0.4.3" +tracing = "0.1.40" [dev-dependencies] +tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tokio-test = "0.4.2" tower-http = { version = "0.4.0", features = ["cors"] } diff --git a/README.md b/README.md index 897d8f4..266f98f 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,9 @@ Consider as well to use the [crate unit tests](https://github.com/LeoniePhiline/ This middleware implements token transfer via [custom request headers](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers). -The middleware requires and is built upon [`axum_sessions`](https://docs.rs/axum-sessions/), which in turn uses [`async_session`](https://docs.rs/async-session/). +The middleware requires and is built upon [`tower_sessions`](https://docs.rs/tower-sessions/). -The current version is built for and works with `axum 0.6.x`, `axum-sessions 0.5.x` and `async_session 3.x`. +The current version is built for and works with `axum 0.6.x`, `tower-sessions 0.4.x`. There will be support for `axum 0.7` and later versions. @@ -67,7 +67,7 @@ See ["Our RNGs"](https://rust-random.github.io/book/guide-rngs.html#cryptographi The security of the underlying session is paramount - the CSRF prevention methods applied can only be as secure as the session carrying the server-side token. -- When creating your [SessionLayer](https://docs.rs/axum-sessions/latest/axum_sessions/struct.SessionLayer.html), make sure to use at least 64 bytes of cryptographically secure randomness. +- When creating your [SessionManagerLayer](https://docs.rs/tower-sessions/latest/tower_sessions/struct.SessionManagerLayer.html) - Do not lower the secure defaults: Keep the session cookie's `secure` flag **on**. - Use the strictest possible same-site policy. @@ -105,16 +105,15 @@ Configure your session and CSRF protection layer in your backend application: ```rust use axum::{ + BoxError, body::Body, http::StatusCode, routing::{get, Router}, + error_handling::HandleErrorLayer, }; +use tower::ServiceBuilder; use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; -use axum_sessions::{async_session::MemoryStore, SessionLayer}; -use rand::RngCore; - -let mut secret = [0; 64]; -rand::thread_rng().try_fill_bytes(&mut secret).unwrap(); +use tower_sessions::{MemoryStore, SessionManagerLayer}; async fn handler() -> StatusCode { StatusCode::OK @@ -136,7 +135,12 @@ let app = Router::new() // Default: "_csrf_token" .session_key("_custom_session_key") ) - .layer(SessionLayer::new(MemoryStore::new(), &secret)); + .layer(ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(SessionManagerLayer::new(MemoryStore::default()))); + // Use hyper to run `app` as service and expose on a local port or socket. ``` @@ -175,37 +179,41 @@ Configure your CORS layer, session and CSRF protection layer in your backend app ```rust use axum::{ + BoxError, body::Body, http::{header, Method, StatusCode}, routing::{get, Router}, + error_handling::HandleErrorLayer, }; +use tower::ServiceBuilder; use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; -use axum_sessions::{async_session::MemoryStore, SessionLayer}; -use rand::RngCore; +use tower_sessions::{MemoryStore, SessionManagerLayer}; use tower_http::cors::{AllowOrigin, CorsLayer}; -let mut secret = [0; 64]; -rand::thread_rng().try_fill_bytes(&mut secret).unwrap(); - async fn handler() -> StatusCode { StatusCode::OK } let app = Router::new() - .route("/", get(handler).post(handler)) - .layer( - // See example above for custom layer configuration. - CsrfLayer::new() - ) - .layer(SessionLayer::new(MemoryStore::new(), &secret)) - .layer( - CorsLayer::new() - .allow_origin(AllowOrigin::list(["https://www.example.com".parse().unwrap()])) - .allow_methods([Method::GET, Method::POST]) - .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) - .allow_credentials(true) - .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), -); + .route("/", get(handler).post(handler)) + .layer( + // See example above for custom layer configuration. + CsrfLayer::new() + ) + .layer(ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(SessionManagerLayer::new(MemoryStore::default())) + .layer( + CorsLayer::new() + .allow_origin(AllowOrigin::list(["https://www.example.com".parse().rap()])) + .allow_methods([Method::GET, Method::POST]) + .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) + .allow_credentials(true) + .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), + ) + ); // Use hyper to run `app` as service and expose on a local port or socket. ``` diff --git a/examples/cross-site/Cargo.toml b/examples/cross-site/Cargo.toml index 292a783..086c693 100644 --- a/examples/cross-site/Cargo.toml +++ b/examples/cross-site/Cargo.toml @@ -8,9 +8,8 @@ publish = false [dependencies] axum = "0.6.16" axum-csrf-sync-pattern = { path = "../../" } -axum-sessions = "0.5.0" +tower-sessions = "0.4.3" color-eyre = "0.6.2" -rand = "0.8.5" tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tower = "0.4.13" tower-http = { version = "0.4.0", features = ["cors"] } diff --git a/examples/cross-site/src/main.rs b/examples/cross-site/src/main.rs index 1af3c22..075cec1 100644 --- a/examples/cross-site/src/main.rs +++ b/examples/cross-site/src/main.rs @@ -1,15 +1,17 @@ use std::net::SocketAddr; use axum::{ + BoxError, http::{header, Method, StatusCode}, response::IntoResponse, routing::{get, Router}, + error_handling::HandleErrorLayer, Server, }; +use tower::ServiceBuilder; use axum_csrf_sync_pattern::CsrfLayer; -use axum_sessions::{async_session::MemoryStore, SessionLayer}; +use tower_sessions::{MemoryStore, SessionManagerLayer}; use color_eyre::eyre::{self, eyre, WrapErr}; -use rand::RngCore; use tower_http::cors::{AllowOrigin, CorsLayer}; #[tokio::main] @@ -33,41 +35,41 @@ async fn main() -> eyre::Result<()> { }; let backend = async { - let mut secret = [0; 64]; - rand::thread_rng() - .try_fill_bytes(&mut secret) - .wrap_err("Failed to generate session seed.")?; - let app = Router::new() .route("/", get(get_token).post(post_handler)) .layer(CsrfLayer::new()) - .layer(SessionLayer::new(MemoryStore::new(), &secret)) - .layer( - CorsLayer::new() - .allow_origin(AllowOrigin::list([ - // Allow CORS requests from our frontend. - "http://127.0.0.1:3000" - .parse() - .wrap_err("Failed to parse socket address.")?, - ])) - // Allow GET and POST methods. Adjust to your needs. - .allow_methods([Method::GET, Method::POST]) - .allow_headers([ - // Allow incoming CORS requests to use the Content-Type header, - header::CONTENT_TYPE, - // as well as the `CsrfLayer` default request header. - "X-CSRF-TOKEN" + .layer(ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(SessionManagerLayer::new(MemoryStore::default())) + .layer( + CorsLayer::new() + .allow_origin(AllowOrigin::list([ + // Allow CORS requests from our frontend. + "http://127.0.0.1:3000" + .parse() + .wrap_err("Failed to parse socket address.")?, + ])) + // Allow GET and POST methods. Adjust to your needs. + .allow_methods([Method::GET, Method::POST]) + .allow_headers([ + // Allow incoming CORS requests to use the Content-Type header, + header::CONTENT_TYPE, + // as well as the `CsrfLayer` default request header. + "X-CSRF-TOKEN" + .parse() + .wrap_err("Failed to parse token header.")?, + ]) + // Allow CORS requests with session cookies. + .allow_credentials(true) + // Instruct the browser to allow JavaScript on the configured origin + // to read the `CsrfLayer` default response header. + .expose_headers(["X-CSRF-TOKEN" .parse() - .wrap_err("Failed to parse token header.")?, - ]) - // Allow CORS requests with session cookies. - .allow_credentials(true) - // Instruct the browser to allow JavaScript on the configured origin - // to read the `CsrfLayer` default response header. - .expose_headers(["X-CSRF-TOKEN" - .parse() - .wrap_err("Failed to parse token header.")?]), - ); + .wrap_err("Failed to parse token header.")?]), + )); + serve(app, 4000).await?; diff --git a/examples/same-site/Cargo.toml b/examples/same-site/Cargo.toml index bfd218e..e50842e 100644 --- a/examples/same-site/Cargo.toml +++ b/examples/same-site/Cargo.toml @@ -8,9 +8,8 @@ publish = false [dependencies] axum = "0.6.16" axum-csrf-sync-pattern = { path = "../../" } -axum-sessions = "0.5.0" +tower-sessions = "0.4.3" color-eyre = "0.6.2" -rand = "0.8.5" tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tower = "0.4.13" tracing-subscriber = "0.3.16" diff --git a/examples/same-site/src/main.rs b/examples/same-site/src/main.rs index c35ae49..5168b3e 100644 --- a/examples/same-site/src/main.rs +++ b/examples/same-site/src/main.rs @@ -1,13 +1,15 @@ use axum::{ + BoxError, http::{header, StatusCode}, response::IntoResponse, routing::get, + error_handling::HandleErrorLayer, Server, }; +use tower::ServiceBuilder; use axum_csrf_sync_pattern::CsrfLayer; -use axum_sessions::{async_session::MemoryStore, SessionLayer}; +use tower_sessions::{MemoryStore, SessionManagerLayer}; use color_eyre::eyre::{self, eyre, WrapErr}; -use rand::RngCore; #[tokio::main] async fn main() -> eyre::Result<()> { @@ -20,15 +22,14 @@ async fn main() -> eyre::Result<()> { .map_err(|e| eyre!(e)) .wrap_err("Failed to initialize tracing-subscriber.")?; - let mut secret = [0; 64]; - rand::thread_rng() - .try_fill_bytes(&mut secret) - .wrap_err("Failed to generate session seed.")?; - let app = axum::Router::new() .route("/", get(index).post(handler)) .layer(CsrfLayer::new()) - .layer(SessionLayer::new(MemoryStore::new(), &secret)); + .layer(ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(SessionManagerLayer::new(MemoryStore::default()))); // Visit "http://127.0.0.1:3000/" in your browser. Server::try_bind( diff --git a/src/lib.rs b/src/lib.rs index d351a14..7fe13cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,7 +9,7 @@ //! //! This middleware implements token transfer via [custom request headers](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers). //! -//! The middleware requires and is built upon [`axum_sessions`](https://docs.rs/axum-sessions/), which in turn uses [`async_session`](https://docs.rs/async-session/). +//! The middleware requires and is built upon [`tower_sessions`](https://docs.rs/tower-sessions/). //! //! The [Same Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) prevents the custom request header to be set by foreign scripts. //! @@ -43,7 +43,7 @@ //! //! The security of the underlying session is paramount - the CSRF prevention methods applied can only be as secure as the session carrying the server-side token. //! -//! - When creating your [SessionLayer](https://docs.rs/axum-sessions/latest/axum_sessions/struct.SessionLayer.html), make sure to use at least 64 bytes of cryptographically secure randomness. +//! - When creating your [SessionManagerLayer](https://docs.rs/tower-sessions/latest/tower_sessions/struct.SessionManagerLayer.html) //! - Do not lower the secure defaults: Keep the session cookie's `secure` flag **on**. //! - Use the strictest possible same-site policy. //! @@ -79,17 +79,16 @@ //! //! ```rust //! use axum::{ +//! BoxError, //! body::Body, //! http::StatusCode, //! routing::{get, Router}, +//! error_handling::HandleErrorLayer, //! }; +//! use tower::ServiceBuilder; //! use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; -//! use axum_sessions::{async_session::MemoryStore, SessionLayer}; -//! use rand::RngCore; -//! -//! let mut secret = [0; 64]; -//! rand::thread_rng().try_fill_bytes(&mut secret).unwrap(); -//! +//! use tower_sessions::{MemoryStore, SessionManagerLayer}; +//! //! async fn handler() -> StatusCode { //! StatusCode::OK //! } @@ -110,7 +109,12 @@ //! // Default: "_csrf_token" //! .session_key("_custom_session_key") //! ) -//! .layer(SessionLayer::new(MemoryStore::new(), &secret)); +//! .layer(ServiceBuilder::new() +//! .layer(HandleErrorLayer::new(|_: BoxError| async { +//! StatusCode::BAD_REQUEST +//! })) +//! .layer(SessionManagerLayer::new(MemoryStore::default())) +//! ); //! //! // Use hyper to run `app` as service and expose on a local port or socket. //! @@ -150,18 +154,17 @@ //! //! ```rust //! use axum::{ +//! BoxError, //! body::Body, //! http::{header, Method, StatusCode}, //! routing::{get, Router}, +//! error_handling::HandleErrorLayer, //! }; +//! use tower::ServiceBuilder; //! use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; -//! use axum_sessions::{async_session::MemoryStore, SessionLayer}; -//! use rand::RngCore; +//! use tower_sessions::{MemoryStore, SessionManagerLayer}; //! use tower_http::cors::{AllowOrigin, CorsLayer}; -//! -//! let mut secret = [0; 64]; -//! rand::thread_rng().try_fill_bytes(&mut secret).unwrap(); -//! +//! //! async fn handler() -> StatusCode { //! StatusCode::OK //! } @@ -172,15 +175,21 @@ //! // See example above for custom layer configuration. //! CsrfLayer::new() //! ) -//! .layer(SessionLayer::new(MemoryStore::new(), &secret)) -//! .layer( -//! CorsLayer::new() -//! .allow_origin(AllowOrigin::list(["https://www.example.com".parse().unwrap()])) -//! .allow_methods([Method::GET, Method::POST]) -//! .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) -//! .allow_credentials(true) -//! .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), -//! ); +//! .layer(ServiceBuilder::new() +//! .layer(HandleErrorLayer::new(|_: BoxError| async { +//! StatusCode::BAD_REQUEST +//! })) +//! .layer(SessionManagerLayer::new(MemoryStore::default())) +//! .layer( +//! CorsLayer::new() +//! .allow_origin(AllowOrigin::list(["https://www.example.com".parse().unwrap()])) +//! .allow_methods([Method::GET, Method::POST]) +//! .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) +//! .allow_credentials(true) +//! .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), +//! ) +//! ); +//! //! //! // Use hyper to run `app` as service and expose on a local port or socket. //! @@ -238,10 +247,9 @@ use std::{ use axum::http::{self, HeaderValue, Request, StatusCode}; use axum_core::response::{IntoResponse, Response}; -use axum_sessions::{async_session::Session, SessionHandle}; +use tower_sessions::Session; use base64::prelude::*; use rand::RngCore; -use tokio::sync::RwLockWriteGuard; use tower::Layer; /// Use `CsrfLayer::new()` to provide the middleware and configuration to axum's service stack. @@ -317,12 +325,12 @@ impl CsrfLayer { fn regenerate_token( &self, - session_write: &mut RwLockWriteGuard, + session: &Session, ) -> Result { let mut buf = [0; 32]; rand::thread_rng().try_fill_bytes(&mut buf)?; let token = BASE64_STANDARD.encode(buf); - session_write.insert(self.session_key, &token)?; + session.insert(self.session_key, &token)?; Ok(token) } @@ -382,8 +390,8 @@ enum Error { #[error("Random number generator error")] Rng(#[from] rand::Error), - #[error("Serde JSON error")] - Serde(#[from] axum_sessions::async_session::serde_json::Error), + #[error("Session error")] + Session(#[from] tower_sessions::session::Error), #[error("Session extension missing. Is `axum_sessions::SessionLayer` installed and layered around the `axum_csrf_sync_pattern::CsrfLayer`?")] SessionLayerMissing, @@ -463,9 +471,9 @@ where let mut inner = std::mem::replace(&mut self.inner, clone); let layer = self.layer; Box::pin(async move { - let session_handle = match req + let session = match req .extensions() - .get::() + .get::() .ok_or(Error::SessionLayerMissing) { Ok(session_handle) => session_handle, @@ -474,10 +482,9 @@ where // Extract the CSRF server side token from the session; create a new one if none has been set yet. // If the regeneration option is set to "per request", then regenerate the token even if present in the session. - let mut session_write = session_handle.write().await; - let mut server_token = match session_write.get::(layer.session_key) { + let mut server_token = match session.get::(layer.session_key).ok().flatten() { Some(token) => token, - None => match layer.regenerate_token(&mut session_write) { + None => match layer.regenerate_token(&session) { Ok(token) => token, Err(error) => return Ok(error.into_response()), }, @@ -518,7 +525,7 @@ where if layer.regenerate_token == RegenerateToken::PerRequest || (!req.method().is_safe() && layer.regenerate_token == RegenerateToken::PerUse) { - server_token = match layer.regenerate_token(&mut session_write) { + server_token = match layer.regenerate_token(&session) { Ok(token) => token, Err(error) => { return Ok(layer.response_with_token(error.into_response(), &server_token)) @@ -526,8 +533,6 @@ where }; } - drop(session_write); - let response = inner.call(req).await.into_response(); // Add X-CSRF-TOKEN response header. @@ -540,14 +545,14 @@ where mod tests { use std::convert::Infallible; - use axum::{body::Body, routing::get, Router}; - use axum_core::response::{IntoResponse, Response}; - use axum_sessions::{async_session::MemoryStore, extractors::ReadableSession, SessionLayer}; + use axum::{body::Body, routing::get, Router, error_handling::HandleErrorLayer}; + use axum_core::{response::{IntoResponse, Response}, BoxError}; + use tower_sessions::{MemoryStore, SessionManagerLayer}; use http::{ header::{COOKIE, SET_COOKIE}, Method, Request, StatusCode, }; - use tower::{Service, ServiceExt}; + use tower::{Service, ServiceExt, ServiceBuilder}; use super::*; @@ -559,17 +564,19 @@ mod tests { .into_response()) } - fn session_layer() -> SessionLayer { - let mut secret = [0; 64]; - rand::thread_rng().try_fill_bytes(&mut secret).unwrap(); - SessionLayer::new(MemoryStore::new(), &secret) + fn session_layer() -> SessionManagerLayer { + SessionManagerLayer::new(MemoryStore::default()) } fn app(csrf_layer: CsrfLayer) -> Router { Router::new() .route("/", get(handler).post(handler)) .layer(csrf_layer) - .layer(session_layer()) + .layer(ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(session_layer())) } #[tokio::test] @@ -869,8 +876,8 @@ mod tests { async fn uses_custom_session_key() { // Custom handler asserting the layer's configured session key is set, // and its value looks like a CSRF token. - async fn extract_session(session: ReadableSession) -> StatusCode { - let session_csrf_token: String = session.get("custom_session_key").unwrap(); + async fn extract_session(session: Session) -> StatusCode { + let session_csrf_token: String = session.get("custom_session_key").unwrap().unwrap(); assert_eq!( BASE64_STANDARD.decode(session_csrf_token).unwrap().len(), @@ -882,7 +889,11 @@ mod tests { let app = Router::new() .route("/", get(extract_session)) .layer(CsrfLayer::new().session_key("custom_session_key")) - .layer(session_layer()); + .layer(ServiceBuilder::new() + .layer(HandleErrorLayer::new(|_: BoxError| async { + StatusCode::BAD_REQUEST + })) + .layer(session_layer())); let response = app .oneshot(Request::builder().body(Body::empty()).unwrap()) From 8ead8e2f77c0c00773fb773460057cf19fe2109e Mon Sep 17 00:00:00 2001 From: daly4 Date: Sat, 18 Nov 2023 08:46:05 -0700 Subject: [PATCH 2/4] remove axum core import --- Cargo.toml | 1 - src/lib.rs | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d121b5..1b3b6fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ maintenance = { status = "actively-developed" } [dependencies] axum = "0.6.20" -axum-core = "0.3.4" base64 = "0.21.5" rand = "0.8.5" thiserror = "1.0.40" diff --git a/src/lib.rs b/src/lib.rs index 7fe13cc..9029e59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -245,8 +245,10 @@ use std::{ task::{Context, Poll}, }; -use axum::http::{self, HeaderValue, Request, StatusCode}; -use axum_core::response::{IntoResponse, Response}; +use axum::{ + http::{self, HeaderValue, Request, StatusCode}, + response::{IntoResponse, Response}, +}; use tower_sessions::Session; use base64::prelude::*; use rand::RngCore; @@ -545,8 +547,15 @@ where mod tests { use std::convert::Infallible; - use axum::{body::Body, routing::get, Router, error_handling::HandleErrorLayer}; - use axum_core::{response::{IntoResponse, Response}, BoxError}; + use axum::{ + body::Body, + routing::get, + Router, + error_handling::HandleErrorLayer, + response::{IntoResponse, Response}, + BoxError, + }; + use tower_sessions::{MemoryStore, SessionManagerLayer}; use http::{ header::{COOKIE, SET_COOKIE}, From 5022de72f3da2917228fe5106cbe957b27039e91 Mon Sep 17 00:00:00 2001 From: daly4 Date: Sun, 7 Jan 2024 22:22:34 -0700 Subject: [PATCH 3/4] update to axum 0.7.3 and tower-sessions 0.9.1 --- Cargo.toml | 12 ++-- README.md | 52 ++++++---------- examples/cross-site/Cargo.toml | 9 ++- examples/cross-site/src/main.rs | 70 ++++++++++----------- examples/same-site/Cargo.toml | 7 +-- examples/same-site/src/main.rs | 27 +++----- src/lib.rs | 105 +++++++++++++++----------------- 7 files changed, 120 insertions(+), 162 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1b3b6fd..e217494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,16 +16,18 @@ edition = "2021" maintenance = { status = "actively-developed" } [dependencies] -axum = "0.6.20" +axum = "0.7.3" base64 = "0.21.5" rand = "0.8.5" thiserror = "1.0.40" -tower = "0.4.13" -tower-cookies = "0.9.0" -tower-sessions = "0.4.3" +tower-cookies = "0.10.0" +tower-layer = "0.3.2" +tower-service = "0.3.2" +tower-sessions = "0.9.1" tracing = "0.1.40" [dev-dependencies] tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tokio-test = "0.4.2" -tower-http = { version = "0.4.0", features = ["cors"] } +tower-http = { version = "0.5.0", features = ["cors"] } +tower = "0.4.13" diff --git a/README.md b/README.md index 266f98f..b998e32 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@ This middleware implements token transfer via [custom request headers](https://c The middleware requires and is built upon [`tower_sessions`](https://docs.rs/tower-sessions/). -The current version is built for and works with `axum 0.6.x`, `tower-sessions 0.4.x`. - -There will be support for `axum 0.7` and later versions. +The current version is built for and works with `axum 0.7.x`, `tower-sessions 0.9.x`. The [Same Origin Policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) prevents the custom request header to be set by foreign scripts. @@ -105,14 +103,11 @@ Configure your session and CSRF protection layer in your backend application: ```rust use axum::{ - BoxError, - body::Body, - http::StatusCode, - routing::{get, Router}, - error_handling::HandleErrorLayer, + http::{header, StatusCode}, + response::IntoResponse, + routing::get, }; -use tower::ServiceBuilder; -use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; +use axum_csrf_sync_pattern::CsrfLayer; use tower_sessions::{MemoryStore, SessionManagerLayer}; async fn handler() -> StatusCode { @@ -135,12 +130,7 @@ let app = Router::new() // Default: "_csrf_token" .session_key("_custom_session_key") ) - .layer(ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::BAD_REQUEST - })) - .layer(SessionManagerLayer::new(MemoryStore::default()))); - + .layer(SessionManagerLayer::new(MemoryStore::default())); // Use hyper to run `app` as service and expose on a local port or socket. ``` @@ -179,16 +169,13 @@ Configure your CORS layer, session and CSRF protection layer in your backend app ```rust use axum::{ - BoxError, - body::Body, http::{header, Method, StatusCode}, + response::IntoResponse, routing::{get, Router}, - error_handling::HandleErrorLayer, }; -use tower::ServiceBuilder; -use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; -use tower_sessions::{MemoryStore, SessionManagerLayer}; +use axum_csrf_sync_pattern::CsrfLayer; use tower_http::cors::{AllowOrigin, CorsLayer}; +use tower_sessions::{MemoryStore, SessionManagerLayer}; async fn handler() -> StatusCode { StatusCode::OK @@ -200,19 +187,14 @@ let app = Router::new() // See example above for custom layer configuration. CsrfLayer::new() ) - .layer(ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::BAD_REQUEST - })) - .layer(SessionManagerLayer::new(MemoryStore::default())) - .layer( - CorsLayer::new() - .allow_origin(AllowOrigin::list(["https://www.example.com".parse().rap()])) - .allow_methods([Method::GET, Method::POST]) - .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) - .allow_credentials(true) - .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), - ) + .layer(SessionManagerLayer::new(MemoryStore::default())) + .layer( + CorsLayer::new() + .allow_origin(AllowOrigin::list(["https://www.example.com".parse().rap()])) + .allow_methods([Method::GET, Method::POST]) + .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) + .allow_credentials(true) + .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), ); // Use hyper to run `app` as service and expose on a local port or socket. diff --git a/examples/cross-site/Cargo.toml b/examples/cross-site/Cargo.toml index 086c693..fa5f7f2 100644 --- a/examples/cross-site/Cargo.toml +++ b/examples/cross-site/Cargo.toml @@ -6,11 +6,10 @@ edition = "2021" publish = false [dependencies] -axum = "0.6.16" +axum = "0.7.3" axum-csrf-sync-pattern = { path = "../../" } -tower-sessions = "0.4.3" +tower-sessions = "0.9.1" color-eyre = "0.6.2" tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } -tower = "0.4.13" -tower-http = { version = "0.4.0", features = ["cors"] } -tracing-subscriber = "0.3.16" +tower-http = { version = "0.5.0", features = ["cors"] } +tracing-subscriber = "0.3.18" diff --git a/examples/cross-site/src/main.rs b/examples/cross-site/src/main.rs index 075cec1..597a872 100644 --- a/examples/cross-site/src/main.rs +++ b/examples/cross-site/src/main.rs @@ -1,18 +1,14 @@ use std::net::SocketAddr; use axum::{ - BoxError, http::{header, Method, StatusCode}, response::IntoResponse, routing::{get, Router}, - error_handling::HandleErrorLayer, - Server, }; -use tower::ServiceBuilder; use axum_csrf_sync_pattern::CsrfLayer; -use tower_sessions::{MemoryStore, SessionManagerLayer}; use color_eyre::eyre::{self, eyre, WrapErr}; use tower_http::cors::{AllowOrigin, CorsLayer}; +use tower_sessions::{MemoryStore, SessionManagerLayer}; #[tokio::main] async fn main() -> eyre::Result<()> { @@ -38,38 +34,33 @@ async fn main() -> eyre::Result<()> { let app = Router::new() .route("/", get(get_token).post(post_handler)) .layer(CsrfLayer::new()) - .layer(ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::BAD_REQUEST - })) - .layer(SessionManagerLayer::new(MemoryStore::default())) - .layer( - CorsLayer::new() - .allow_origin(AllowOrigin::list([ - // Allow CORS requests from our frontend. - "http://127.0.0.1:3000" - .parse() - .wrap_err("Failed to parse socket address.")?, - ])) - // Allow GET and POST methods. Adjust to your needs. - .allow_methods([Method::GET, Method::POST]) - .allow_headers([ - // Allow incoming CORS requests to use the Content-Type header, - header::CONTENT_TYPE, - // as well as the `CsrfLayer` default request header. - "X-CSRF-TOKEN" - .parse() - .wrap_err("Failed to parse token header.")?, - ]) - // Allow CORS requests with session cookies. - .allow_credentials(true) - // Instruct the browser to allow JavaScript on the configured origin - // to read the `CsrfLayer` default response header. - .expose_headers(["X-CSRF-TOKEN" + .layer(SessionManagerLayer::new(MemoryStore::default())) + .layer( + CorsLayer::new() + .allow_origin(AllowOrigin::list([ + // Allow CORS requests from our frontend. + "http://127.0.0.1:3000" .parse() - .wrap_err("Failed to parse token header.")?]), - )); - + .wrap_err("Failed to parse socket address.")?, + ])) + // Allow GET and POST methods. Adjust to your needs. + .allow_methods([Method::GET, Method::POST]) + .allow_headers([ + // Allow incoming CORS requests to use the Content-Type header, + header::CONTENT_TYPE, + // as well as the `CsrfLayer` default request header. + "X-CSRF-TOKEN" + .parse() + .wrap_err("Failed to parse token header.")?, + ]) + // Allow CORS requests with session cookies. + .allow_credentials(true) + // Instruct the browser to allow JavaScript on the configured origin + // to read the `CsrfLayer` default response header. + .expose_headers(["X-CSRF-TOKEN" + .parse() + .wrap_err("Failed to parse token header.")?]), + ); serve(app, 4000).await?; @@ -83,9 +74,10 @@ async fn main() -> eyre::Result<()> { async fn serve(app: Router, port: u16) -> eyre::Result<()> { let addr = SocketAddr::from(([127, 0, 0, 1], port)); - Server::try_bind(&addr) - .wrap_err("Could not bind to network address.")? - .serve(app.into_make_service()) + let listener = tokio::net::TcpListener::bind(addr) + .await + .wrap_err("Could not bind to network address.")?; + axum::serve(listener, app) .await .wrap_err("Failed to serve the app.")?; diff --git a/examples/same-site/Cargo.toml b/examples/same-site/Cargo.toml index e50842e..6870229 100644 --- a/examples/same-site/Cargo.toml +++ b/examples/same-site/Cargo.toml @@ -6,10 +6,9 @@ edition = "2021" publish = false [dependencies] -axum = "0.6.16" +axum = "0.7.3" axum-csrf-sync-pattern = { path = "../../" } -tower-sessions = "0.4.3" +tower-sessions = "0.9.1" color-eyre = "0.6.2" tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } -tower = "0.4.13" -tracing-subscriber = "0.3.16" +tracing-subscriber = "0.3.18" \ No newline at end of file diff --git a/examples/same-site/src/main.rs b/examples/same-site/src/main.rs index 5168b3e..7eb62d3 100644 --- a/examples/same-site/src/main.rs +++ b/examples/same-site/src/main.rs @@ -1,15 +1,11 @@ use axum::{ - BoxError, http::{header, StatusCode}, response::IntoResponse, routing::get, - error_handling::HandleErrorLayer, - Server, }; -use tower::ServiceBuilder; use axum_csrf_sync_pattern::CsrfLayer; -use tower_sessions::{MemoryStore, SessionManagerLayer}; use color_eyre::eyre::{self, eyre, WrapErr}; +use tower_sessions::{MemoryStore, SessionManagerLayer}; #[tokio::main] async fn main() -> eyre::Result<()> { @@ -25,22 +21,15 @@ async fn main() -> eyre::Result<()> { let app = axum::Router::new() .route("/", get(index).post(handler)) .layer(CsrfLayer::new()) - .layer(ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::BAD_REQUEST - })) - .layer(SessionManagerLayer::new(MemoryStore::default()))); + .layer(SessionManagerLayer::new(MemoryStore::default())); // Visit "http://127.0.0.1:3000/" in your browser. - Server::try_bind( - &"0.0.0.0:3000" - .parse() - .wrap_err("Failed to parse socket address.")?, - ) - .wrap_err("Could not bind to network address.")? - .serve(app.into_make_service()) - .await - .wrap_err("Failed to serve the app.")?; + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") + .await + .wrap_err("Could not bind to network address.")?; + axum::serve(listener, app) + .await + .wrap_err("Failed to serve the app.")?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 9029e59..116c9a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,10 +85,9 @@ //! routing::{get, Router}, //! error_handling::HandleErrorLayer, //! }; -//! use tower::ServiceBuilder; //! use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; //! use tower_sessions::{MemoryStore, SessionManagerLayer}; -//! +//! //! async fn handler() -> StatusCode { //! StatusCode::OK //! } @@ -109,12 +108,7 @@ //! // Default: "_csrf_token" //! .session_key("_custom_session_key") //! ) -//! .layer(ServiceBuilder::new() -//! .layer(HandleErrorLayer::new(|_: BoxError| async { -//! StatusCode::BAD_REQUEST -//! })) -//! .layer(SessionManagerLayer::new(MemoryStore::default())) -//! ); +//! .layer(SessionManagerLayer::new(MemoryStore::default())); //! //! // Use hyper to run `app` as service and expose on a local port or socket. //! @@ -164,7 +158,7 @@ //! use axum_csrf_sync_pattern::{CsrfLayer, RegenerateToken}; //! use tower_sessions::{MemoryStore, SessionManagerLayer}; //! use tower_http::cors::{AllowOrigin, CorsLayer}; -//! +//! //! async fn handler() -> StatusCode { //! StatusCode::OK //! } @@ -175,19 +169,14 @@ //! // See example above for custom layer configuration. //! CsrfLayer::new() //! ) -//! .layer(ServiceBuilder::new() -//! .layer(HandleErrorLayer::new(|_: BoxError| async { -//! StatusCode::BAD_REQUEST -//! })) -//! .layer(SessionManagerLayer::new(MemoryStore::default())) -//! .layer( -//! CorsLayer::new() -//! .allow_origin(AllowOrigin::list(["https://www.example.com".parse().unwrap()])) -//! .allow_methods([Method::GET, Method::POST]) -//! .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) -//! .allow_credentials(true) -//! .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), -//! ) +//! .layer(SessionManagerLayer::new(MemoryStore::default())) +//! .layer( +//! CorsLayer::new() +//! .allow_origin(AllowOrigin::list(["https://www.example.com".parse().unwrap()])) +//! .allow_methods([Method::GET, Method::POST]) +//! .allow_headers([header::CONTENT_TYPE, "X-CSRF-TOKEN".parse().unwrap()]) +//! .allow_credentials(true) +//! .expose_headers(["X-CSRF-TOKEN".parse().unwrap()]), //! ); //! //! @@ -249,10 +238,10 @@ use axum::{ http::{self, HeaderValue, Request, StatusCode}, response::{IntoResponse, Response}, }; -use tower_sessions::Session; use base64::prelude::*; use rand::RngCore; -use tower::Layer; +use tower_layer::Layer; +use tower_sessions::Session; /// Use `CsrfLayer::new()` to provide the middleware and configuration to axum's service stack. /// @@ -325,14 +314,11 @@ impl CsrfLayer { self } - fn regenerate_token( - &self, - session: &Session, - ) -> Result { + async fn regenerate_token(&self, session: &Session) -> Result { let mut buf = [0; 32]; rand::thread_rng().try_fill_bytes(&mut buf)?; let token = BASE64_STANDARD.encode(buf); - session.insert(self.session_key, &token)?; + session.insert(self.session_key, &token).await?; Ok(token) } @@ -395,7 +381,7 @@ enum Error { #[error("Session error")] Session(#[from] tower_sessions::session::Error), - #[error("Session extension missing. Is `axum_sessions::SessionLayer` installed and layered around the `axum_csrf_sync_pattern::CsrfLayer`?")] + #[error("Session extension missing. Is `tower_sessions::SessionLayer` installed and layered around the `axum_csrf_sync_pattern::CsrfLayer`?")] SessionLayerMissing, #[error("Incoming CSRF token header was not valid ASCII")] @@ -441,8 +427,8 @@ pub struct CsrfMiddleware { } impl CsrfMiddleware { - /// Create a new middleware from an inner [`tower::Service`] (axum-specific bounds, such as `Infallible` errors apply!) and a [`CsrfLayer`]. - /// Commonly, the middleware is created by the [`tower::Layer`] - and never manually. + /// Create a new middleware from an inner [`tower_service::Service`] (axum-specific bounds, such as `Infallible` errors apply!) and a [`CsrfLayer`]. + /// Commonly, the middleware is created by the [`tower_layer::Layer`] - and never manually. pub fn new(inner: S, layer: CsrfLayer) -> Self { CsrfMiddleware { inner, layer } } @@ -454,9 +440,12 @@ impl CsrfMiddleware { } } -impl tower::Service> for CsrfMiddleware +impl tower_service::Service> for CsrfMiddleware where - S: tower::Service, Response = Response, Error = Infallible> + Send + Clone + 'static, + S: tower_service::Service, Response = Response, Error = Infallible> + + Send + + Clone + + 'static, S::Future: Send, { type Response = S::Response; @@ -484,9 +473,14 @@ where // Extract the CSRF server side token from the session; create a new one if none has been set yet. // If the regeneration option is set to "per request", then regenerate the token even if present in the session. - let mut server_token = match session.get::(layer.session_key).ok().flatten() { + let mut server_token = match session + .get::(layer.session_key) + .await + .ok() + .flatten() + { Some(token) => token, - None => match layer.regenerate_token(&session) { + None => match layer.regenerate_token(&session).await { Ok(token) => token, Err(error) => return Ok(error.into_response()), }, @@ -527,7 +521,7 @@ where if layer.regenerate_token == RegenerateToken::PerRequest || (!req.method().is_safe() && layer.regenerate_token == RegenerateToken::PerUse) { - server_token = match layer.regenerate_token(&session) { + server_token = match layer.regenerate_token(&session).await { Ok(token) => token, Err(error) => { return Ok(layer.response_with_token(error.into_response(), &server_token)) @@ -549,19 +543,16 @@ mod tests { use axum::{ body::Body, + response::{IntoResponse, Response}, routing::get, Router, - error_handling::HandleErrorLayer, - response::{IntoResponse, Response}, - BoxError, }; - - use tower_sessions::{MemoryStore, SessionManagerLayer}; use http::{ header::{COOKIE, SET_COOKIE}, Method, Request, StatusCode, }; - use tower::{Service, ServiceExt, ServiceBuilder}; + use tower::{Service, ServiceExt}; + use tower_sessions::{MemoryStore, SessionManagerLayer}; use super::*; @@ -581,11 +572,7 @@ mod tests { Router::new() .route("/", get(handler).post(handler)) .layer(csrf_layer) - .layer(ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::BAD_REQUEST - })) - .layer(session_layer())) + .layer(session_layer()) } #[tokio::test] @@ -624,6 +611,7 @@ mod tests { // Get CSRF token let response = app + .as_service() .ready() .await .unwrap() @@ -644,6 +632,7 @@ mod tests { // Use CSRF token for POST request let response = app + .as_service() .ready() .await .unwrap() @@ -666,6 +655,7 @@ mod tests { // Attempt token re-use for a second POST request let response = app + .as_service() .ready() .await .unwrap() @@ -693,6 +683,7 @@ mod tests { // Get single-use CSRF token let response = app + .as_service() .ready() .await .unwrap() @@ -713,6 +704,7 @@ mod tests { // Use CSRF token for POST request let response = app + .as_service() .ready() .await .unwrap() @@ -735,6 +727,7 @@ mod tests { // Attempt token re-use for a second POST request let response = app + .as_service() .ready() .await .unwrap() @@ -762,6 +755,7 @@ mod tests { // Get single-use CSRF token let response = app + .as_service() .ready() .await .unwrap() @@ -782,6 +776,7 @@ mod tests { // Perform another GET request let response = app + .as_service() .ready() .await .unwrap() @@ -803,6 +798,7 @@ mod tests { // Attempt using single-request token for POST request let response = app + .as_service() .ready() .await .unwrap() @@ -830,6 +826,7 @@ mod tests { // Get CSRF token let response = app + .as_service() .ready() .await .unwrap() @@ -847,6 +844,7 @@ mod tests { // Use CSRF token for POST request let response = app + .as_service() .ready() .await .unwrap() @@ -886,7 +884,8 @@ mod tests { // Custom handler asserting the layer's configured session key is set, // and its value looks like a CSRF token. async fn extract_session(session: Session) -> StatusCode { - let session_csrf_token: String = session.get("custom_session_key").unwrap().unwrap(); + let session_csrf_token: String = + session.get("custom_session_key").await.unwrap().unwrap(); assert_eq!( BASE64_STANDARD.decode(session_csrf_token).unwrap().len(), @@ -898,11 +897,7 @@ mod tests { let app = Router::new() .route("/", get(extract_session)) .layer(CsrfLayer::new().session_key("custom_session_key")) - .layer(ServiceBuilder::new() - .layer(HandleErrorLayer::new(|_: BoxError| async { - StatusCode::BAD_REQUEST - })) - .layer(session_layer())); + .layer(session_layer()); let response = app .oneshot(Request::builder().body(Body::empty()).unwrap()) @@ -931,7 +926,7 @@ mod tests { let layer = CsrfLayer::new(); let response = Response::builder() .status(StatusCode::OK) - .body(axum::body::boxed(Body::empty())) + .body(Body::empty()) .unwrap(); let response = layer.response_with_token(response, "\n"); From 89729fe9ee339c75ecc9e46e3a9bf493dd4f0f5a Mon Sep 17 00:00:00 2001 From: daly4 Date: Mon, 8 Jan 2024 21:35:51 -0700 Subject: [PATCH 4/4] fix cargo sort --- Cargo.toml | 2 +- examples/cross-site/Cargo.toml | 2 +- examples/same-site/Cargo.toml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e217494..94e38e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,5 @@ tracing = "0.1.40" [dev-dependencies] tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tokio-test = "0.4.2" -tower-http = { version = "0.5.0", features = ["cors"] } tower = "0.4.13" +tower-http = { version = "0.5.0", features = ["cors"] } diff --git a/examples/cross-site/Cargo.toml b/examples/cross-site/Cargo.toml index fa5f7f2..1265440 100644 --- a/examples/cross-site/Cargo.toml +++ b/examples/cross-site/Cargo.toml @@ -8,8 +8,8 @@ publish = false [dependencies] axum = "0.7.3" axum-csrf-sync-pattern = { path = "../../" } -tower-sessions = "0.9.1" color-eyre = "0.6.2" tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } tower-http = { version = "0.5.0", features = ["cors"] } +tower-sessions = "0.9.1" tracing-subscriber = "0.3.18" diff --git a/examples/same-site/Cargo.toml b/examples/same-site/Cargo.toml index 6870229..25bb695 100644 --- a/examples/same-site/Cargo.toml +++ b/examples/same-site/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] axum = "0.7.3" axum-csrf-sync-pattern = { path = "../../" } -tower-sessions = "0.9.1" color-eyre = "0.6.2" tokio = { version = "1.27.0", features = ["macros", "rt", "rt-multi-thread"] } -tracing-subscriber = "0.3.18" \ No newline at end of file +tower-sessions = "0.9.1" +tracing-subscriber = "0.3.18"