feat: implement CSRF protection across forms and routes

This commit is contained in:
2026-05-09 22:09:19 +02:00
parent e8874f9220
commit d89d373a91
14 changed files with 147 additions and 8 deletions

View File

@@ -0,0 +1,58 @@
use axum::{
extract::Request,
http::{HeaderValue, header},
middleware::Next,
response::Response,
};
#[derive(Clone)]
pub struct CsrfToken(pub String);
pub fn extract_from_cookie(headers: &axum::http::HeaderMap) -> Option<String> {
headers
.get(header::COOKIE)
.and_then(|v| v.to_str().ok())
.and_then(|cookies| {
cookies
.split(';')
.find_map(|c| c.trim().strip_prefix("csrf=").map(str::to_string))
})
}
fn secure_flag() -> &'static str {
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") {
"; Secure"
} else {
""
}
}
pub async fn csrf_middleware(mut req: Request, next: Next) -> Response {
let existing = extract_from_cookie(req.headers());
let (token, needs_set) = match existing {
Some(t) => (t, false),
None => (uuid::Uuid::new_v4().to_string(), true),
};
req.extensions_mut().insert(CsrfToken(token.clone()));
let mut response = next.run(req).await;
if needs_set {
let cookie = format!(
"csrf={}; HttpOnly; Path=/; SameSite=Strict{}",
token,
secure_flag()
);
if let Ok(val) = HeaderValue::from_str(&cookie) {
response.headers_mut().append(header::SET_COOKIE, val);
}
}
response
}
/// Returns true if the form token does not match the cookie token.
pub fn mismatch(token: &CsrfToken, form_value: &str) -> bool {
token.0 != form_value || form_value.is_empty()
}