Skip to main content

aster_forge_middleware/axum/
security_headers.rs

1//! Axum security-header adapter.
2pub use crate::shared::security_headers::{
3    REFERRER_POLICY_VALUE, X_CONTENT_TYPE_OPTIONS_VALUE, X_FRAME_OPTIONS_VALUE,
4};
5use axum::{extract::Request, middleware::Next, response::Response};
6use http::header::{HeaderName, HeaderValue};
7
8/// Supplies missing defaults, preserving explicit product response policies.
9pub async fn security_headers(request: Request, next: Next) -> Response {
10    let mut response = next.run(request).await;
11    for (name, value) in [
12        ("x-frame-options", X_FRAME_OPTIONS_VALUE),
13        ("referrer-policy", REFERRER_POLICY_VALUE),
14        ("x-content-type-options", X_CONTENT_TYPE_OPTIONS_VALUE),
15    ] {
16        response
17            .headers_mut()
18            .entry(HeaderName::from_static(name))
19            .or_insert(HeaderValue::from_static(value));
20    }
21    response
22}
23
24#[cfg(test)]
25mod tests {
26    use super::*;
27    use axum::{Router, body::Body, middleware, response::IntoResponse, routing::get};
28    use http::{Request as HttpRequest, StatusCode};
29    use tower::ServiceExt;
30
31    #[tokio::test]
32    async fn adds_defaults_and_preserves_explicit_product_headers_on_errors() {
33        let router = Router::new()
34            .route("/", get(|| async { "called" }))
35            .route(
36                "/strict",
37                get(|| async {
38                    let mut response = StatusCode::BAD_REQUEST.into_response();
39                    response
40                        .headers_mut()
41                        .insert("referrer-policy", HeaderValue::from_static("no-referrer"));
42                    response
43                        .headers_mut()
44                        .insert("x-frame-options", HeaderValue::from_static("DENY"));
45                    response
46                }),
47            )
48            .layer(middleware::from_fn(security_headers));
49        for (path, referrer, frame, status) in [
50            (
51                "/",
52                REFERRER_POLICY_VALUE,
53                X_FRAME_OPTIONS_VALUE,
54                StatusCode::OK,
55            ),
56            ("/strict", "no-referrer", "DENY", StatusCode::BAD_REQUEST),
57            (
58                "/missing",
59                REFERRER_POLICY_VALUE,
60                X_FRAME_OPTIONS_VALUE,
61                StatusCode::NOT_FOUND,
62            ),
63        ] {
64            let response = router
65                .clone()
66                .oneshot(
67                    HttpRequest::builder()
68                        .uri(path)
69                        .body(Body::empty())
70                        .unwrap(),
71                )
72                .await
73                .unwrap();
74            assert_eq!(response.status(), status);
75            assert_eq!(response.headers()["referrer-policy"], referrer);
76            assert_eq!(response.headers()["x-frame-options"], frame);
77            assert_eq!(
78                response.headers()["x-content-type-options"],
79                X_CONTENT_TYPE_OPTIONS_VALUE
80            );
81        }
82    }
83}