Files
twofauth-rs/src/cors.rs
T
wt cf25e28c28
Rust verification / test (push) Successful in 4m10s
initial commit
2026-10-28 15:59:54 +07:00

140 lines
4.7 KiB
Rust

//! The pinned application's default CORS policy: public API origins, no credentials.
use axum::{
extract::Request,
http::{HeaderValue, Method, StatusCode, header},
middleware::Next,
response::{IntoResponse, Response},
};
pub async fn handle(request: Request, next: Next) -> Response {
let path = request.uri().path();
let applies = path.starts_with("/api/") || path == "/sanctum/csrf-cookie";
if !applies {
return next.run(request).await;
}
let method = request
.headers()
.get(header::ACCESS_CONTROL_REQUEST_METHOD)
.cloned();
let requested = request
.headers()
.get(header::ACCESS_CONTROL_REQUEST_HEADERS)
.cloned();
let preflight = request.method() == Method::OPTIONS && method.is_some();
let mut response = if preflight {
StatusCode::NO_CONTENT.into_response()
} else {
next.run(request).await
};
response.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
HeaderValue::from_static("*"),
);
if preflight {
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=UTF-8"),
);
response.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_METHODS,
HeaderValue::from_bytes(&method.unwrap().as_bytes().to_ascii_uppercase())
.expect("validated request header"),
);
response.headers_mut().insert(
header::ACCESS_CONTROL_ALLOW_HEADERS,
requested.unwrap_or(HeaderValue::from_static("")),
);
response.headers_mut().insert(
header::ACCESS_CONTROL_MAX_AGE,
HeaderValue::from_static("0"),
);
response.headers_mut().append(
header::VARY,
HeaderValue::from_static(
"Access-Control-Request-Method, Access-Control-Request-Headers",
),
);
}
response
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{Router, body::Body, http::Request, middleware, routing::get};
use tower::ServiceExt;
#[tokio::test]
async fn preflight_bypasses_handlers_and_never_enables_credentials() {
let app = Router::new()
.route("/api/v1/probe", get(|| async { StatusCode::UNAUTHORIZED }))
.route("/user", get(|| async { StatusCode::UNAUTHORIZED }))
.layer(middleware::from_fn(handle));
let response = app
.clone()
.oneshot(
Request::builder()
.method("OPTIONS")
.uri("/api/v1/probe")
.header("origin", "https://example.invalid")
.header("access-control-request-method", "POST")
.header(
"access-control-request-headers",
"Authorization,Content-Type",
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
assert_eq!(
response.headers()["content-type"],
"text/html; charset=UTF-8"
);
assert_eq!(response.headers()["access-control-allow-origin"], "*");
assert_eq!(response.headers()["access-control-allow-methods"], "POST");
assert!(
!response
.headers()
.contains_key("access-control-allow-credentials")
);
let response = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/probe")
.header("origin", "https://example.invalid")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
assert_eq!(response.headers()["access-control-allow-origin"], "*");
let without_origin = app
.clone()
.oneshot(
Request::builder()
.uri("/api/v1/probe")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(without_origin.headers()["access-control-allow-origin"], "*");
let response = app
.oneshot(
Request::builder()
.uri("/user")
.header("origin", "https://example.invalid")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert!(
!response
.headers()
.contains_key("access-control-allow-origin")
);
}
}