use std::sync::Arc;
use axum::extract::Request;
use axum::http::header::{HeaderValue, LOCATION};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::any;
use axum::Router;
use super::{forbidden, unauthorized, Auth, AuthDecision};
use crate::config::AuthConfig;
pub struct ForwardAuth {
auth: Arc<Auth>,
path: String,
login_url: Option<String>,
}
impl ForwardAuth {
pub fn build(config: &AuthConfig, auth: Arc<Auth>) -> Option<Arc<Self>> {
let fa = config.forward_auth.as_ref()?;
if !fa.enabled {
return None;
}
Some(Arc::new(Self {
auth,
path: fa.path.clone(),
login_url: fa.login_url.clone(),
}))
}
pub fn routes<S>(self: &Arc<Self>) -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
let fa = self.clone();
Router::new().route(
&self.path,
any(move |req: Request| {
let fa = fa.clone();
async move { fa.verify(req).await }
}),
)
}
async fn verify(&self, request: Request) -> Response {
let headers = request.headers();
let method = original_method(headers)
.unwrap_or_else(|| request.method().as_str().to_ascii_uppercase());
let path = original_path(headers).unwrap_or_else(|| request.uri().path().to_string());
match self.auth.decide(headers, &path, &method).await {
AuthDecision::Allow(claim_headers, _) => {
(StatusCode::OK, claim_headers).into_response()
}
AuthDecision::Unauthenticated(msg) => self.deny(msg),
AuthDecision::Forbidden(msg) => forbidden(msg),
}
}
fn deny(&self, msg: &'static str) -> Response {
let mut response = unauthorized(msg);
if let Some(url) = &self.login_url {
if let Ok(value) = HeaderValue::try_from(url.as_str()) {
response.headers_mut().insert(LOCATION, value);
}
}
response
}
}
fn original_method(headers: &HeaderMap) -> Option<String> {
forwarded(headers, &["x-forwarded-method", "x-original-method"]).map(|m| m.to_ascii_uppercase())
}
fn original_path(headers: &HeaderMap) -> Option<String> {
let raw = forwarded(headers, &["x-forwarded-uri", "x-original-uri"])?;
let path = raw.split_once('?').map_or(raw.as_str(), |(p, _)| p);
Some(path.to_string())
}
fn forwarded(headers: &HeaderMap, names: &[&str]) -> Option<String> {
names
.iter()
.filter_map(|n| headers.get(*n).and_then(|v| v.to_str().ok()))
.find(|v| !v.is_empty())
.map(str::to_string)
}
#[cfg(all(test, feature = "builtin_jwt"))]
mod tests;