59 lines
1.5 KiB
Rust
59 lines
1.5 KiB
Rust
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()
|
|
}
|