Skip to main content

gatel_core/hoops/
forward_auth.rs

1use salvo::http::StatusCode;
2use salvo::{Depot, FlowCtrl, Request, Response, async_trait};
3use tracing::debug;
4
5/// Forwarded authentication middleware.
6///
7/// Delegates authentication to an external service. For every incoming
8/// request the middleware:
9///   1. Clones the request headers (no body) and adds `X-Forwarded-Uri` (original path+query) and
10///      `X-Forwarded-Method` (original method).
11///   2. Sends a GET request to `auth_url` with those headers via reqwest.
12///   3. If the auth service returns 2xx, copies the configured headers from the auth response into
13///      the original request and continues the chain.
14///   4. If the auth service returns non-2xx, that response is returned directly to the client.
15pub struct ForwardAuthHoop {
16    auth_url: String,
17    copy_headers: Vec<String>,
18    client: reqwest::Client,
19}
20
21impl ForwardAuthHoop {
22    pub fn new(auth_url: String, copy_headers: Vec<String>) -> Self {
23        Self {
24            auth_url,
25            copy_headers,
26            client: reqwest::Client::new(),
27        }
28    }
29}
30
31#[async_trait]
32impl salvo::Handler for ForwardAuthHoop {
33    async fn handle(
34        &self,
35        req: &mut Request,
36        depot: &mut Depot,
37        res: &mut Response,
38        ctrl: &mut FlowCtrl,
39    ) {
40        // Build the forwarded-auth request: copy all incoming headers and
41        // add X-Forwarded-Uri / X-Forwarded-Method.
42        let forwarded_uri = req
43            .uri()
44            .path_and_query()
45            .map(|pq| pq.as_str().to_string())
46            .unwrap_or_else(|| "/".to_string());
47        let forwarded_method = req.method().to_string();
48
49        let mut auth_req = self.client.get(&self.auth_url);
50
51        // Copy all incoming request headers to the auth request.
52        for (name, value) in req.headers() {
53            if let Ok(val_str) = value.to_str() {
54                auth_req = auth_req.header(name.as_str(), val_str);
55            }
56        }
57
58        // Append the forwarded metadata headers.
59        auth_req = auth_req
60            .header("X-Forwarded-Uri", &forwarded_uri)
61            .header("X-Forwarded-Method", &forwarded_method);
62
63        let auth_resp = match auth_req.send().await {
64            Ok(r) => r,
65            Err(e) => {
66                debug!(error = %e, "forward-auth request failed");
67                res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
68                res.body(format!("forward-auth request failed: {e}"));
69                ctrl.skip_rest();
70                return;
71            }
72        };
73
74        let auth_status = auth_resp.status();
75
76        if auth_status.is_success() {
77            // Copy the configured headers from the auth response into the
78            // original request so downstream handlers can use them.
79            for header_name in &self.copy_headers {
80                if let Some(value) = auth_resp.headers().get(header_name.as_str())
81                    && let Ok(hn) = header_name.parse::<http::header::HeaderName>()
82                    && let Ok(hv) = http::header::HeaderValue::from_bytes(value.as_bytes())
83                {
84                    req.headers_mut().insert(hn, hv);
85                }
86            }
87
88            debug!(auth_url = %self.auth_url, "forward-auth passed, continuing chain");
89            ctrl.call_next(req, depot, res).await;
90        } else {
91            // Return the auth service's response to the client.
92            debug!(
93                auth_url = %self.auth_url,
94                status = %auth_status,
95                "forward-auth denied, returning auth response"
96            );
97
98            let status =
99                StatusCode::from_u16(auth_status.as_u16()).unwrap_or(StatusCode::UNAUTHORIZED);
100
101            // Save auth response headers before consuming the body.
102            let auth_resp_headers = auth_resp.headers().clone();
103
104            // Collect the auth response body.
105            let body_bytes = auth_resp.bytes().await.unwrap_or_default();
106
107            res.status_code(status);
108
109            // Forward response headers from the auth service.
110            // Skip transfer-encoding as it no longer applies to this response.
111            for (name, value) in &auth_resp_headers {
112                if name.as_str().eq_ignore_ascii_case("transfer-encoding") {
113                    continue;
114                }
115                if let Ok(hv) = http::header::HeaderValue::from_bytes(value.as_bytes()) {
116                    res.headers_mut().insert(name.clone(), hv);
117                }
118            }
119
120            res.body(body_bytes.to_vec());
121            ctrl.skip_rest();
122        }
123    }
124}