🌊 tower-surf

A stateless CSRF middleware for [tower][crate-tower]. [![Crates](https://img.shields.io/crates/v/tower-surf.svg)](https://crates.io/crates/tower-surf) ![MSRV](https://img.shields.io/crates/msrv/tower-surf) [![Docs](https://docs.rs/tower-surf/badge.svg)](https://docs.rs/tower-surf) ![CI](https://github.com/its-danny/tower-surf/actions/workflows/ci.yml/badge.svg)
## 🏄‍♂️ Overview This crate uses the [Double Submit Cookie Pattern][owasp-double-submit] to mitigate CSRF. ### How it works - **Secret key**: You provide a **secret key** used to sign CSRF tokens. This secret is secured by [secstr][crate-secstr] and only in memory as plaintext during the signing and validating processes. For more information on managing your secret key, see [OWASP's Cryptographic Storage Cheat Sheet][owasp-cryptographic-storage]). - **Token creation**: - We generate a **message** by combining a unique **session identifier** with a cryptographically secure **random value** (using the [`rand`][crate-rand] crate). - We then create an **signature** using the **secret key** and the **message**. - The token is formed by concatenating the **signature** and the **message**. - **Token storage**: - The server sends the token to the client in two ways: - As a cookie (handled by us). - In the header of subsequent requests (handled by you). - **Token validation**: - For each incoming request that would mutate state: - We extract the token from the request headers. - We split the token into the **signature** and the **message**. - We recalculate the **signature** using the **secret key** and compare them. - If the **signature** is valid and the token matches the value stored in the cookie, the request is allowed to proceed. ### Cookies By default, the cookies are set to `HTTPOnly`, `SameSite: Strict`, and `Secure`. ## 📦 Install ```toml [dependencies] tower-surf = "0.3.0" ``` ## 🗝️ Usage ### With [`axum`][crate-axum] ```rust use std::net::SocketAddr; use axum::{routing::get, Router}; use http::StatusCode; use tower_surf::{Surf, Token}; #[tokio::main] async fn main() { let app = Router::new() .route("/login", get(login)).route("/logout", get(logout)) .layer(Surf::new("secret-key").secure(false)); 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 login(token: Token) -> Result { token.set("unique-session-id").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(StatusCode::OK) } async fn logout(token: Token) -> StatusCode { token.reset(); StatusCode::OK } ``` > [!NOTE] > See the [examples][examples] for a full example. ## 🥰 Thank you - I read a lot of the [tower-sessions](https://github.com/maxcountryman/tower-sessions) codebase to figure out how to make a tower project. - The [tokio community](https://discord.com/invite/tokio) answered a lot of silly questions. [crate-axum]: https://github.com/tokio-rs/axum [crate-rand]: https://github.com/rust-random/rand [crate-tower]: https://github.com/tower-rs/tower [crate-secstr]: https://codeberg.org/valpackett/secstr [examples]: https://github.com/its-danny/tower-surf/tree/main/examples [owasp-cryptographic-storage]: https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html [owasp-double-submit]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#alternative-using-a-double-submit-cookie-pattern