//! 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") ); } }