structured_proxy/auth/
forward.rs1use std::sync::Arc;
9
10use axum::extract::Request;
11use axum::http::header::{HeaderValue, LOCATION};
12use axum::http::{HeaderMap, StatusCode};
13use axum::response::{IntoResponse, Response};
14use axum::routing::any;
15use axum::Router;
16
17use super::{forbidden, unauthorized, Auth, AuthDecision};
18use crate::config::AuthConfig;
19
20pub struct ForwardAuth {
22 auth: Arc<Auth>,
23 path: String,
24 login_url: Option<String>,
25}
26
27impl ForwardAuth {
28 pub fn build(config: &AuthConfig, auth: Arc<Auth>) -> Option<Arc<Self>> {
33 let fa = config.forward_auth.as_ref()?;
34 if !fa.enabled {
35 return None;
36 }
37 Some(Arc::new(Self {
38 auth,
39 path: fa.path.clone(),
40 login_url: fa.login_url.clone(),
41 }))
42 }
43
44 pub fn routes<S>(self: &Arc<Self>) -> Router<S>
46 where
47 S: Clone + Send + Sync + 'static,
48 {
49 let fa = self.clone();
50 Router::new().route(
53 &self.path,
54 any(move |req: Request| {
55 let fa = fa.clone();
56 async move { fa.verify(req).await }
57 }),
58 )
59 }
60
61 async fn verify(&self, request: Request) -> Response {
62 let headers = request.headers();
63 let method = original_method(headers)
64 .unwrap_or_else(|| request.method().as_str().to_ascii_uppercase());
65 let path = original_path(headers).unwrap_or_else(|| request.uri().path().to_string());
66
67 match self.auth.decide(headers, &path, &method).await {
68 AuthDecision::Allow(claim_headers, _) => {
71 (StatusCode::OK, claim_headers).into_response()
72 }
73 AuthDecision::Unauthenticated(msg) => self.deny(msg),
74 AuthDecision::Forbidden(msg) => forbidden(msg),
75 }
76 }
77
78 fn deny(&self, msg: &'static str) -> Response {
81 let mut response = unauthorized(msg);
82 if let Some(url) = &self.login_url {
83 if let Ok(value) = HeaderValue::try_from(url.as_str()) {
84 response.headers_mut().insert(LOCATION, value);
85 }
86 }
87 response
88 }
89}
90
91fn original_method(headers: &HeaderMap) -> Option<String> {
93 forwarded(headers, &["x-forwarded-method", "x-original-method"]).map(|m| m.to_ascii_uppercase())
94}
95
96fn original_path(headers: &HeaderMap) -> Option<String> {
98 let raw = forwarded(headers, &["x-forwarded-uri", "x-original-uri"])?;
99 let path = raw.split_once('?').map_or(raw.as_str(), |(p, _)| p);
100 Some(path.to_string())
101}
102
103fn forwarded(headers: &HeaderMap, names: &[&str]) -> Option<String> {
105 names
106 .iter()
107 .filter_map(|n| headers.get(*n).and_then(|v| v.to_str().ok()))
108 .find(|v| !v.is_empty())
109 .map(str::to_string)
110}
111
112#[cfg(all(test, feature = "builtin_jwt"))]
115mod tests;