use crate::security::types::{AuthContext, AuthResult};
use axum::{
body::Body,
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
use std::future::Future;
use std::pin::Pin;
pub fn auth_middleware<T: Clone + Send + Sync + 'static>(
_auth: T,
extract_auth: impl Fn(&Request<Body>) -> AuthResult<AuthContext> + Clone + Send + 'static,
) -> impl Fn(Request<Body>, Next) -> Pin<Box<dyn Future<Output = Response> + Send>> + Clone + Send {
move |mut req: Request<Body>, next: Next| {
let extract_auth = extract_auth.clone();
Box::pin(async move {
match extract_auth(&req) {
Ok(auth_context) => {
req.extensions_mut().insert(auth_context);
next.run(req).await
}
Err(_) => {
let mut response = Response::new(Body::from("Unauthorized"));
*response.status_mut() = StatusCode::UNAUTHORIZED;
response
}
}
})
}
}
#[allow(dead_code)] #[inline]
fn extract_client_ip_core(req: &Request<Body>) -> Option<String> {
use axum::extract::connect_info::ConnectInfo;
let trusted_proxies = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.1"];
let direct_ip = req
.extensions()
.get::<ConnectInfo<std::net::SocketAddr>>()
.map(|ci| ci.0.ip().to_string());
let from_trusted_proxy = direct_ip
.as_deref()
.map(|ip| {
trusted_proxies
.iter()
.any(|range| is_ip_in_range(ip, range))
})
.unwrap_or(false);
if from_trusted_proxy {
if let Some(header) = req.headers().get("X-Forwarded-For") {
if let Ok(value) = header.to_str() {
if let Some(ip) = value.split(',').next().map(|s| s.trim()) {
if is_valid_ip(ip) {
return Some(ip.to_string());
}
}
}
}
if let Some(header) = req.headers().get("X-Real-IP") {
if let Ok(ip) = header.to_str() {
if is_valid_ip(ip) {
return Some(ip.to_string());
}
}
}
}
if let Some(ip) = direct_ip {
return Some(ip);
}
if let Some(header) = req.headers().get("X-Real-IP") {
if let Ok(ip) = header.to_str() {
if is_valid_ip(ip) {
return Some(ip.to_string());
}
}
}
if let Some(header) = req.headers().get("X-Forwarded-For") {
if let Ok(value) = header.to_str() {
if let Some(ip) = value.split(',').next().map(|s| s.trim()) {
if is_valid_ip(ip) {
return Some(ip.to_string());
}
}
}
}
None
}
#[allow(dead_code)] fn is_ip_in_range(ip: &str, cidr: &str) -> bool {
let parts: Vec<&str> = cidr.split('/').collect();
if parts.len() != 2 {
return false;
}
let network = parts[0];
let mask_bits: u32 = parts[1].parse().unwrap_or(0);
let ip_bytes: Vec<u8> = ip.split('.').filter_map(|s| s.parse().ok()).collect();
let net_bytes: Vec<u8> = network.split('.').filter_map(|s| s.parse().ok()).collect();
if ip_bytes.len() != 4 || net_bytes.len() != 4 {
return false;
}
let ip_val = (ip_bytes[0] as u32) << 24
| (ip_bytes[1] as u32) << 16
| (ip_bytes[2] as u32) << 8
| ip_bytes[3] as u32;
let net_val = (net_bytes[0] as u32) << 24
| (net_bytes[1] as u32) << 16
| (net_bytes[2] as u32) << 8
| net_bytes[3] as u32;
if mask_bits > 32 {
return false;
}
let mask_val = if mask_bits == 0 {
0
} else {
!0u32 << (32 - mask_bits)
};
(ip_val & mask_val) == (net_val & mask_val)
}
#[allow(dead_code)] fn is_valid_ip(ip: &str) -> bool {
use std::net::IpAddr;
if ip.is_empty() || ip.len() > 45 {
return false;
}
if let Ok(IpAddr::V4(ipv4)) = ip.parse::<IpAddr>() {
let octets = ipv4.octets();
if octets[0] == 10 {
return false;
}
if octets[0] == 172 && octets[1] >= 16 && octets[1] <= 31 {
return false;
}
if octets[0] == 192 && octets[1] == 168 {
return false;
}
if octets[0] == 127 {
return false;
}
if octets[0] == 169 && octets[1] == 254 {
return false;
}
if octets[0] >= 224 && octets[0] <= 239 {
return false;
}
if octets[0] == 0 {
return false;
}
true
} else if let Ok(IpAddr::V6(ipv6)) = ip.parse::<IpAddr>() {
let segments = ipv6.segments();
if segments == [0, 0, 0, 0, 0, 0, 0, 1] {
return false;
}
if segments[0] & 0xffc0 == 0xfe80 {
return false;
}
if segments[0] & 0xfe00 == 0xfc00 {
return false;
}
if segments[0] & 0xff00 == 0xff00 {
return false;
}
if segments == [0, 0, 0, 0, 0, 0, 0, 0] {
return false;
}
true
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_ip() {
assert!(is_valid_ip("8.8.8.8"));
assert!(is_valid_ip("1.1.1.1"));
assert!(!is_valid_ip("10.0.0.1"));
assert!(!is_valid_ip("192.168.1.1"));
assert!(!is_valid_ip("172.16.0.1"));
assert!(!is_valid_ip("127.0.0.1"));
assert!(!is_valid_ip("169.254.1.1"));
assert!(!is_valid_ip("::1"));
assert!(!is_valid_ip(""));
assert!(!is_valid_ip("invalid"));
}
#[test]
fn test_is_ip_in_range() {
assert!(is_ip_in_range("10.0.0.1", "10.0.0.0/8"));
assert!(is_ip_in_range("192.168.1.1", "192.168.0.0/16"));
assert!(!is_ip_in_range("8.8.8.8", "10.0.0.0/8"));
}
#[test]
fn test_is_valid_ip_multicast_rejected() {
assert!(!is_valid_ip("224.0.0.1"));
assert!(!is_valid_ip("239.255.255.255"));
}
#[test]
fn test_is_valid_ip_unspecified_rejected() {
assert!(!is_valid_ip("0.0.0.0"));
assert!(!is_valid_ip("0.1.2.3"));
}
#[test]
fn test_is_valid_ip_ipv6_public_accepted() {
assert!(is_valid_ip("2001:db8::1"));
assert!(is_valid_ip("::ffff:192.0.2.1"));
}
#[test]
fn test_is_valid_ip_ipv6_loopback_rejected() {
assert!(!is_valid_ip("::1"));
}
#[test]
fn test_is_valid_ip_ipv6_link_local_rejected() {
assert!(!is_valid_ip("fe80::1"));
}
#[test]
fn test_is_valid_ip_ipv6_unique_local_rejected() {
assert!(!is_valid_ip("fc00::1"));
}
#[test]
fn test_is_valid_ip_ipv6_multicast_rejected() {
assert!(!is_valid_ip("ff00::1"));
}
#[test]
fn test_is_valid_ip_ipv6_unspecified_rejected() {
assert!(!is_valid_ip("::"));
}
#[test]
fn test_is_valid_ip_invalid_format() {
assert!(!is_valid_ip("not-an-ip"));
assert!(!is_valid_ip("999.999.999.999"));
assert!(!is_valid_ip("256.1.2.3"));
}
#[test]
fn test_is_valid_ip_too_long() {
let long_ip = "a".repeat(50);
assert!(!is_valid_ip(&long_ip));
}
#[test]
fn test_is_ip_in_range_localhost() {
assert!(is_ip_in_range("127.0.0.1", "127.0.0.1/32"));
}
#[test]
fn test_is_ip_in_range_invalid_cidr() {
assert!(!is_ip_in_range("10.0.0.1", "invalid-cidr"));
}
#[test]
fn test_is_ip_in_range_invalid_cidr_format() {
assert!(!is_ip_in_range("10.0.0.1", "10.0.0.0"));
}
#[test]
fn test_is_ip_in_range_boundary() {
assert!(is_ip_in_range("10.0.0.1", "10.0.0.1/32"));
assert!(!is_ip_in_range("10.0.0.2", "10.0.0.1/32"));
}
#[test]
fn test_is_ip_in_range_mask_bits_over_32_returns_false_no_panic() {
assert!(!is_ip_in_range("10.0.0.1", "10.0.0.0/33"));
assert!(!is_ip_in_range("10.0.0.1", "10.0.0.0/128"));
assert!(!is_ip_in_range("10.0.0.1", "10.0.0.0/255"));
}
#[test]
fn test_is_ip_in_range_172_range() {
assert!(is_ip_in_range("172.16.0.1", "172.16.0.0/12"));
assert!(is_ip_in_range("172.31.255.255", "172.16.0.0/12"));
assert!(!is_ip_in_range("172.32.0.1", "172.16.0.0/12"));
}
#[tokio::test]
async fn test_auth_middleware_is_clone_send_sync() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Ok(AuthContext {
user_id: Some("test".to_string()),
permissions: vec![],
metadata: crate::security::types::AuthMetadata::default(),
})
};
let middleware = auth_middleware((), extract_auth);
fn check<T: Send + Sync>(_: T) {}
check(middleware.clone());
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_returns_closure() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Err(crate::security::types::AuthError::MissingAuth)
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_extract_client_ip_x_forwarded_for_single() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Forwarded-For", "8.8.8.8".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("8.8.8.8".to_string()));
}
#[test]
fn test_extract_client_ip_x_forwarded_for_multiple() {
let mut req = Request::new(Body::empty());
req.headers_mut().insert(
"X-Forwarded-For",
"8.8.8.8, 10.0.0.1, 192.168.1.1".parse().unwrap(),
);
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("8.8.8.8".to_string()));
}
#[test]
fn test_extract_client_ip_trusted_proxy_trusts_x_forwarded_for() {
use axum::extract::connect_info::ConnectInfo;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let mut req = Request::new(Body::empty());
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8080);
req.extensions_mut().insert(ConnectInfo(addr));
req.headers_mut()
.insert("X-Forwarded-For", "203.0.113.5".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("203.0.113.5".to_string()));
}
#[test]
fn test_extract_client_ip_non_trusted_proxy_ignores_headers() {
use axum::extract::connect_info::ConnectInfo;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let mut req = Request::new(Body::empty());
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 8080);
req.extensions_mut().insert(ConnectInfo(addr));
req.headers_mut()
.insert("X-Forwarded-For", "1.2.3.4".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("8.8.8.8".to_string()));
}
#[test]
fn test_extract_client_ip_x_real_ip() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Real-IP", "8.8.8.8".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("8.8.8.8".to_string()));
}
#[test]
fn test_extract_client_ip_connect_info_fallback() {
use axum::extract::connect_info::ConnectInfo;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let mut req = Request::new(Body::empty());
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 8080);
req.extensions_mut().insert(ConnectInfo(addr));
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("192.168.1.100".to_string()));
}
#[test]
fn test_extract_client_ip_no_headers_returns_none() {
let req = Request::new(Body::empty());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[test]
fn test_extract_client_ip_x_forwarded_for_invalid_ip() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Forwarded-For", "not-a-valid-ip".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[test]
fn test_extract_client_ip_x_real_ip_invalid_value() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Real-IP", "not-an-ip".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[test]
fn test_extract_client_ip_x_real_ip_private_rejected() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Real-IP", "10.0.0.1".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[test]
fn test_extract_client_ip_x_forwarded_for_private_rejected_by_is_valid_ip() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Forwarded-For", "10.0.0.1".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[tokio::test]
async fn test_auth_middleware_passes_with_valid_context() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Ok(AuthContext {
user_id: Some("user123".to_string()),
permissions: vec!["read".to_string()],
metadata: crate::security::types::AuthMetadata::default(),
})
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_rejects_invalid_auth() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Err(crate::security::types::AuthError::InvalidToken)
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_auth_middleware_missing_auth_header() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Err(crate::security::types::AuthError::MissingAuth)
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_auth_middleware_preserves_request_body() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Ok(AuthContext {
user_id: Some("user".to_string()),
permissions: vec![],
metadata: crate::security::types::AuthMetadata::default(),
})
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route(
"/test",
axum::routing::post(|body: Body| async move {
let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap();
format!("len={}", bytes.len())
}),
)
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder()
.method("POST")
.uri("/test")
.body(Body::from("hello body"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_preserves_extensions() {
use axum::extract::Request as ExtractRequest;
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Ok(AuthContext {
user_id: Some("user".to_string()),
permissions: vec!["admin".to_string()],
metadata: crate::security::types::AuthMetadata::default(),
})
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route(
"/test",
axum::routing::get(|req: ExtractRequest| async move {
let ctx = req.extensions().get::<AuthContext>();
let user = ctx.and_then(|c| c.user_id.clone()).unwrap_or_default();
format!("user={}", user)
}),
)
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[test]
fn test_is_valid_ip_ipv4_public() {
assert!(is_valid_ip("8.8.8.8"));
assert!(is_valid_ip("1.1.1.1"));
assert!(is_valid_ip("203.0.113.50"));
}
#[test]
fn test_is_valid_ip_ipv6_public() {
assert!(is_valid_ip("2001:db8::1"));
assert!(is_valid_ip("2001:0db8:85a3:0000:0000:8a2e:0370:7334"));
}
#[test]
fn test_is_valid_ip_hostname_not_ip() {
assert!(!is_valid_ip("example.com"));
assert!(!is_valid_ip("localhost"));
assert!(!is_valid_ip("not-an-ip-at-all"));
}
#[test]
fn test_is_ip_in_range_single_ip_no_mask() {
assert!(!is_ip_in_range("10.0.0.1", "10.0.0.1"));
assert!(!is_ip_in_range("192.168.1.1", "192.168.1.1"));
}
#[test]
fn test_extract_client_ip_x_forwarded_for_empty() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Forwarded-For", "".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[test]
fn test_extract_client_ip_x_forwarded_for_whitespace() {
let mut req = Request::new(Body::empty());
req.headers_mut()
.insert("X-Forwarded-For", " ".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, None);
}
#[tokio::test]
async fn test_auth_middleware_with_clone() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Ok(AuthContext {
user_id: Some("clone-test".to_string()),
permissions: vec![],
metadata: crate::security::types::AuthMetadata::default(),
})
};
let middleware = auth_middleware((), extract_auth);
let cloned = middleware.clone();
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(cloned));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_success_path_via_router() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Ok(AuthContext {
user_id: Some("test".to_string()),
permissions: vec![],
metadata: crate::security::types::AuthMetadata::default(),
})
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_auth_middleware_unauthorized_path_via_router() {
let extract_auth = |_req: &Request<Body>| -> AuthResult<AuthContext> {
Err(crate::security::types::AuthError::MissingAuth)
};
let middleware = auth_middleware((), extract_auth);
let router = axum::Router::new()
.route("/test", axum::routing::get(|| async { "ok" }))
.layer(axum::middleware::from_fn(middleware));
let response = tower::ServiceExt::oneshot(
router,
Request::builder().uri("/test").body(Body::empty()).unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_is_ip_in_range_non_ipv4_address() {
assert!(!is_ip_in_range("not-an-ip", "10.0.0.0/8"));
assert!(!is_ip_in_range("::1", "10.0.0.0/8"));
}
#[test]
fn test_is_ip_in_range_zero_mask_bits() {
assert!(is_ip_in_range("8.8.8.8", "10.0.0.0/0"));
assert!(is_ip_in_range("1.2.3.4", "0.0.0.0/0"));
}
#[test]
fn test_extract_client_ip_trusted_proxy_uses_x_real_ip_when_no_x_forwarded_for() {
use axum::extract::connect_info::ConnectInfo;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let mut req = Request::new(Body::empty());
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8080);
req.extensions_mut().insert(ConnectInfo(addr));
req.headers_mut()
.insert("X-Real-IP", "203.0.113.50".parse().unwrap());
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("203.0.113.50".to_string()));
}
#[test]
fn test_extract_client_ip_trusted_proxy_x_forwarded_for_multiple_ips() {
use axum::extract::connect_info::ConnectInfo;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
let mut req = Request::new(Body::empty());
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 8080);
req.extensions_mut().insert(ConnectInfo(addr));
req.headers_mut().insert(
"X-Forwarded-For",
"203.0.113.5, 198.51.100.10, 8.8.8.8".parse().unwrap(),
);
let ip = extract_client_ip_core(&req);
assert_eq!(ip, Some("203.0.113.5".to_string()));
}
}