74 lines
2.6 KiB
Rust
74 lines
2.6 KiB
Rust
//! CSRF double-submit cookie middleware.
|
|
//!
|
|
//! State-changing requests (`POST`, `PUT`, `DELETE`, `PATCH`) that were
|
|
//! authenticated via an HttpOnly cookie (i.e. browser sessions) **must**
|
|
//! include an `X-CSRF-Token` header whose value matches the `oxicloud_csrf`
|
|
//! cookie. Requests authenticated via `Bearer` or `Basic` headers are
|
|
//! exempt because they are not vulnerable to CSRF — the browser never
|
|
//! attaches those automatically.
|
|
//!
|
|
//! Safe methods (`GET`, `HEAD`, `OPTIONS`) are always allowed through.
|
|
|
|
use axum::{
|
|
extract::Request,
|
|
http::{Method, StatusCode},
|
|
middleware::Next,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
|
|
use crate::interfaces::api::cookie_auth;
|
|
use crate::interfaces::middleware::auth::CookieAuthenticated;
|
|
|
|
/// Methods considered safe (no side-effects) — CSRF check is skipped.
|
|
const SAFE_METHODS: [Method; 3] = [Method::GET, Method::HEAD, Method::OPTIONS];
|
|
|
|
/// Middleware that enforces CSRF protection for cookie-authenticated browser
|
|
/// sessions using the **double-submit cookie** pattern.
|
|
///
|
|
/// Must be applied **after** `auth_middleware` so that the
|
|
/// `CookieAuthenticated` marker is available in extensions.
|
|
pub async fn csrf_middleware(request: Request, next: Next) -> Result<Response, Response> {
|
|
// Safe methods never need CSRF validation.
|
|
if SAFE_METHODS.contains(request.method()) {
|
|
return Ok(next.run(request).await);
|
|
}
|
|
|
|
// Only enforce for cookie-authenticated sessions.
|
|
let is_cookie_auth = request.extensions().get::<CookieAuthenticated>().is_some();
|
|
if !is_cookie_auth {
|
|
return Ok(next.run(request).await);
|
|
}
|
|
|
|
// Extract the CSRF token from the cookie.
|
|
let cookie_token =
|
|
cookie_auth::extract_cookie_value(request.headers(), cookie_auth::CSRF_COOKIE);
|
|
|
|
// Extract the CSRF token from the request header.
|
|
let header_token = request
|
|
.headers()
|
|
.get(cookie_auth::CSRF_HEADER)
|
|
.and_then(|v| v.to_str().ok())
|
|
.map(|s| s.to_string());
|
|
|
|
match (cookie_token, header_token) {
|
|
(Some(c), Some(h)) if !c.is_empty() && c == h => {
|
|
// Tokens match — allow the request through.
|
|
Ok(next.run(request).await)
|
|
}
|
|
_ => {
|
|
tracing::warn!(
|
|
method = %request.method(),
|
|
uri = %request.uri(),
|
|
"CSRF validation failed: missing or mismatched token"
|
|
);
|
|
Err((
|
|
StatusCode::FORBIDDEN,
|
|
axum::Json(serde_json::json!({
|
|
"error": "CSRF token missing or invalid"
|
|
})),
|
|
)
|
|
.into_response())
|
|
}
|
|
}
|
|
}
|