1use crate::auth::{AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE, AuthMethod};
4use crate::core::audit::middleware::record_authenticated_principal;
5use crate::core::models::{ApiKey, user::types::User};
6use crate::core::types::context::{RequestContext, SharedRequestContext};
7use crate::server::middleware::auth_rate_limiter::get_auth_rate_limiter;
8use crate::server::middleware::helpers::{
9 extract_auth_method_with_api_key_header, is_public_route, middleware_gateway_error_response,
10};
11use crate::server::middleware::rate_limit::{
12 AuthRateLimitReservation, RateLimitError, enforce_rate_limit_for_rejected_auth,
13 network_client_key, reserve_rate_limit_for_auth_attempt,
14};
15use crate::server::routes::ai::{self, api_key_allows_endpoint, check_permission};
16use crate::server::state::AppState;
17use crate::utils::error::gateway_error::GatewayError;
18use actix_web::body::EitherBody;
19use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready};
20use actix_web::{HttpMessage, HttpRequest, web};
21use futures::future::{Ready, ready};
22use std::collections::HashMap;
23use std::future::Future;
24use std::pin::Pin;
25use std::rc::Rc;
26use std::sync::Arc;
27use tracing::{debug, error, warn};
28
29pub struct AuthMiddleware;
31
32fn bypasses_header_auth(path: &str, auth_method: &AuthMethod) -> bool {
33 if path == "/health/ready" {
34 return matches!(auth_method, AuthMethod::None);
35 }
36 is_public_route(path) || path == "/auth/refresh"
37}
38
39impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
40where
41 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
42 S::Future: 'static,
43 B: 'static,
44{
45 type Response = ServiceResponse<EitherBody<B>>;
46 type Error = actix_web::Error;
47 type InitError = ();
48 type Transform = AuthMiddlewareService<S>;
49 type Future = Ready<Result<Self::Transform, Self::InitError>>;
50
51 fn new_transform(&self, service: S) -> Self::Future {
52 ready(Ok(AuthMiddlewareService {
53 service: Rc::new(service),
54 }))
55 }
56}
57
58pub struct AuthMiddlewareService<S> {
60 service: Rc<S>,
61}
62
63impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
64where
65 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
66 S::Future: 'static,
67 B: 'static,
68{
69 type Response = ServiceResponse<EitherBody<B>>;
70 type Error = actix_web::Error;
71 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
72
73 forward_ready!(service);
74
75 fn call(&self, mut req: ServiceRequest) -> Self::Future {
76 let service = Rc::clone(&self.service);
77
78 Box::pin(async move {
79 let app_state = match req.app_data::<web::Data<AppState>>().cloned() {
80 Some(state) => state,
81 None => {
82 return Err(actix_web::error::ErrorInternalServerError(
83 "Missing application state",
84 ));
85 }
86 };
87 let cfg = app_state.config.load();
88 let enable_jwt = cfg.auth().enable_jwt;
89 let enable_api_key = cfg.auth().enable_api_key;
90 let api_key_header = cfg.auth().api_key_header.clone();
91 let rate_limit_enabled = cfg.gateway.rate_limit.enabled;
92 let rate_limit_rpm = cfg.gateway.rate_limit.effective_rpm();
93 let trusted_proxies = cfg.server().trusted_proxies.clone();
94
95 let context = build_request_context(&mut req);
96 let auth_method =
97 extract_auth_method_with_api_key_header(req.headers(), api_key_header.as_str());
98 let is_public = bypasses_header_auth(req.path(), &auth_method);
99 let client_id = get_client_identifier(&req, &trusted_proxies);
100 let network_rate_limit_enabled = rate_limit_enabled && client_id.is_some();
101 let rate_limiter = get_auth_rate_limiter();
102
103 if is_public {
104 insert_request_context(&mut req, context);
105 return service
106 .call(req)
107 .await
108 .map(ServiceResponse::map_into_left_body);
109 }
110
111 let auth_enabled = enable_jwt || enable_api_key;
112 if !auth_enabled {
113 if !cfg.auth().allow_anonymous {
119 error!(
120 "All authentication methods are disabled and allow_anonymous is false; \
121 rejecting request to non-public route. Enable JWT or API key auth, or \
122 set allow_anonymous: true (development only)."
123 );
124 return Ok(unauthorized_response(
125 req,
126 "Authentication is not configured",
127 ));
128 }
129 insert_request_context(&mut req, context);
130 return service
131 .call(req)
132 .await
133 .map(ServiceResponse::map_into_left_body);
134 }
135
136 let mut auth_rate_limit_reservation = if requires_auth_verification(&auth_method) {
140 match reserve_gateway_rate_limit_before_auth(
141 &req,
142 network_rate_limit_enabled,
143 rate_limit_rpm,
144 &trusted_proxies,
145 )
146 .await
147 {
148 Ok(reservation) => Some(reservation),
149 Err(error) => return Ok(rate_limit_response(req, error)),
150 }
151 } else {
152 None
153 };
154
155 let auth_attempt = match rate_limiter
156 .reserve_network_attempt(client_id.as_deref())
157 .await
158 {
159 Ok(reservation) => reservation,
160 Err(wait_seconds) => {
161 if auth_rate_limit_reservation.is_none()
162 && let Err(error) = enforce_gateway_rate_limit_for_auth_rejection(
163 &req,
164 network_rate_limit_enabled,
165 rate_limit_rpm,
166 &trusted_proxies,
167 )
168 .await
169 {
170 return Ok(rate_limit_response(req, error));
171 }
172 return Ok(failed_attempt_rate_limit_response(req, wait_seconds));
173 }
174 };
175
176 let auth_method = match auth_method {
177 AuthMethod::Jwt(_) if !enable_jwt => {
178 auth_attempt.record_failure();
179 if auth_rate_limit_reservation.is_none()
180 && let Err(error) = enforce_gateway_rate_limit_for_auth_rejection(
181 &req,
182 network_rate_limit_enabled,
183 rate_limit_rpm,
184 &trusted_proxies,
185 )
186 .await
187 {
188 return Ok(rate_limit_response(req, error));
189 }
190 return Ok(unauthorized_response(req, "JWT authentication disabled"));
191 }
192 AuthMethod::ApiKey(_) if !enable_api_key => {
193 auth_attempt.record_failure();
194 if auth_rate_limit_reservation.is_none()
195 && let Err(error) = enforce_gateway_rate_limit_for_auth_rejection(
196 &req,
197 network_rate_limit_enabled,
198 rate_limit_rpm,
199 &trusted_proxies,
200 )
201 .await
202 {
203 return Ok(rate_limit_response(req, error));
204 }
205 return Ok(unauthorized_response(
206 req,
207 "API key authentication disabled",
208 ));
209 }
210 other => other,
211 };
212
213 if matches!(auth_method, AuthMethod::None) {
214 auth_attempt.record_failure();
215 if let Err(error) = enforce_gateway_rate_limit_for_auth_rejection(
216 &req,
217 network_rate_limit_enabled,
218 rate_limit_rpm,
219 &trusted_proxies,
220 )
221 .await
222 {
223 return Ok(rate_limit_response(req, error));
224 }
225 return Ok(unauthorized_response(req, "Missing authentication"));
226 }
227
228 match app_state.auth.authenticate(auth_method, context).await {
229 Ok(result) if result.success => {
230 auth_attempt.release();
231 debug!("Authentication succeeded");
237
238 record_authenticated_principal(&req, &result.context);
241 insert_request_context(&mut req, result.context);
242 match api_key_allows_endpoint(result.api_key.as_ref(), req.path()) {
243 Ok(true) => {}
244 Ok(false) => {
245 warn!("Authenticated API key is not permitted to access this endpoint");
246 return Ok(forbidden_response(
247 req,
248 "API key is not permitted for this endpoint",
249 ));
250 }
251 Err(error) => {
252 warn!("Authenticated API key policy is invalid: {}", error);
253 if let Some(reservation) = auth_rate_limit_reservation.take() {
254 reservation.release().await;
255 }
256 return Ok(authentication_unavailable_response(req));
257 }
258 }
259 if let Some(operation) = ai::operation_for_path(req.path())
260 && !check_permission(
261 result.user.as_ref(),
262 result.api_key.as_ref(),
263 operation,
264 )
265 {
266 warn!(
267 "Authenticated caller is not permitted to access AI operation '{}'",
268 operation
269 );
270 return Ok(forbidden_response(
271 req,
272 "API key is not permitted for this operation",
273 ));
274 }
275
276 if let Some(user) = result.user {
277 req.extensions_mut().insert::<User>(user);
278 }
279 if let Some(api_key) = result.api_key {
280 req.extensions_mut().insert::<ApiKey>(api_key);
281 }
282
283 if let Some(reservation) = auth_rate_limit_reservation.take() {
284 reservation.release().await;
285 }
286
287 service
288 .call(req)
289 .await
290 .map(ServiceResponse::map_into_left_body)
291 }
292 Ok(result) => {
293 auth_attempt.record_failure();
294 warn!(
295 "Authentication failed: {}",
296 result
297 .error
298 .clone()
299 .unwrap_or_else(|| "unauthorized".to_string())
300 );
301 if auth_rate_limit_reservation.is_none()
302 && let Err(error) = enforce_gateway_rate_limit_for_auth_rejection(
303 &req,
304 network_rate_limit_enabled,
305 rate_limit_rpm,
306 &trusted_proxies,
307 )
308 .await
309 {
310 return Ok(rate_limit_response(req, error));
311 }
312 Ok(unauthorized_response(
313 req,
314 result.error.unwrap_or_else(|| "Unauthorized".to_string()),
315 ))
316 }
317 Err(err) => {
318 auth_attempt.release();
319 if let Some(reservation) = auth_rate_limit_reservation.take() {
320 reservation.release().await;
321 }
322 error!(error = %err, "Authentication infrastructure failure");
323 Ok(authentication_unavailable_response(req))
324 }
325 }
326 })
327 }
328}
329
330async fn enforce_gateway_rate_limit_for_auth_rejection(
331 req: &ServiceRequest,
332 enabled: bool,
333 requests_per_minute: u32,
334 trusted_proxies: &[String],
335) -> Result<(), RateLimitError> {
336 if !enabled {
337 return Ok(());
338 }
339
340 enforce_rate_limit_for_rejected_auth(req, requests_per_minute, trusted_proxies).await
341}
342
343async fn reserve_gateway_rate_limit_before_auth(
344 req: &ServiceRequest,
345 enabled: bool,
346 requests_per_minute: u32,
347 trusted_proxies: &[String],
348) -> Result<AuthRateLimitReservation, RateLimitError> {
349 if !enabled {
350 return Ok(AuthRateLimitReservation::noop());
351 }
352
353 reserve_rate_limit_for_auth_attempt(req, requests_per_minute, trusted_proxies).await
354}
355
356fn requires_auth_verification(auth_method: &AuthMethod) -> bool {
357 matches!(
358 auth_method,
359 AuthMethod::Jwt(_) | AuthMethod::ApiKey(_) | AuthMethod::Session(_)
360 )
361}
362
363fn unauthorized_response<B>(
364 req: ServiceRequest,
365 message: impl Into<String>,
366) -> ServiceResponse<EitherBody<B>> {
367 let message = message.into();
368 middleware_gateway_error_response(
369 req,
370 actix_web::error::ErrorUnauthorized(message.clone()),
371 GatewayError::Auth(message),
372 )
373}
374
375fn forbidden_response<B>(
376 req: ServiceRequest,
377 message: impl Into<String>,
378) -> ServiceResponse<EitherBody<B>> {
379 let message = message.into();
380 middleware_gateway_error_response(
381 req,
382 actix_web::error::ErrorForbidden(message.clone()),
383 GatewayError::Forbidden(message),
384 )
385}
386
387fn authentication_unavailable_response<B>(req: ServiceRequest) -> ServiceResponse<EitherBody<B>> {
388 if req.path() == "/v1/messages" {
389 return req
390 .into_response(ai::messages::error_response(&GatewayError::Internal(
391 AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE.into(),
392 )))
393 .map_into_right_body();
394 }
395 if ai::is_openai_compatible_path(req.path()) {
396 return req
397 .into_response(ai::openai_internal_error_response(
398 AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE,
399 ))
400 .map_into_right_body();
401 }
402
403 req.error_response(actix_web::error::ErrorInternalServerError(
404 AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE,
405 ))
406 .map_into_right_body()
407}
408
409fn rate_limit_response<B>(
410 req: ServiceRequest,
411 error: RateLimitError,
412) -> ServiceResponse<EitherBody<B>> {
413 let gateway_error = error.gateway_error();
414 middleware_gateway_error_response(req, actix_web::Error::from(error), gateway_error)
415}
416
417fn failed_attempt_rate_limit_response<B>(
418 req: ServiceRequest,
419 wait_seconds: u64,
420) -> ServiceResponse<EitherBody<B>> {
421 let message = format!(
422 "Too many failed attempts. Try again in {} seconds",
423 wait_seconds
424 );
425 middleware_gateway_error_response(
426 req,
427 actix_web::error::ErrorTooManyRequests(message.clone()),
428 GatewayError::RateLimit {
429 message,
430 retry_after: Some(wait_seconds),
431 rpm_limit: None,
432 tpm_limit: None,
433 },
434 )
435}
436
437pub fn get_request_context(req: &HttpRequest) -> Result<SharedRequestContext, actix_web::Error> {
439 if let Some(context) = req.extensions().get::<SharedRequestContext>() {
440 return Ok(Arc::clone(context));
441 }
442
443 req.extensions()
444 .get::<RequestContext>()
445 .map(|context| Arc::new(context.clone()))
446 .ok_or_else(|| actix_web::error::ErrorInternalServerError("Missing request context"))
447}
448
449fn insert_request_context(req: &mut ServiceRequest, context: RequestContext) {
450 req.extensions_mut()
451 .insert::<SharedRequestContext>(Arc::new(context));
452}
453
454fn get_client_identifier(req: &ServiceRequest, trusted_proxies: &[String]) -> Option<String> {
463 req.peer_addr()
464 .map(|_| network_client_key(req, trusted_proxies))
465}
466
467fn build_request_context(req: &mut ServiceRequest) -> RequestContext {
468 let mut context = RequestContext::new();
469
470 if let Some(id) = req
474 .headers()
475 .get("x-request-id")
476 .and_then(|value| value.to_str().ok())
477 .filter(|s| !s.is_empty())
478 {
479 context.request_id = id.to_string();
480 }
481
482 context.user_agent = req
483 .headers()
484 .get("user-agent")
485 .and_then(|value| value.to_str().ok())
486 .map(str::to_string);
487 context.client_ip = req.connection_info().peer_addr().map(|ip| ip.to_string());
488
489 let mut headers = HashMap::new();
490 for (name, value) in req.headers().iter() {
491 if name.as_str().eq_ignore_ascii_case("authorization")
492 || name.as_str().eq_ignore_ascii_case("x-api-key")
493 {
494 continue;
495 }
496 if let Ok(value) = value.to_str() {
497 headers.insert(name.as_str().to_string(), value.to_string());
498 }
499 }
500 context.headers = headers;
501
502 context
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use actix_web::test::TestRequest;
509
510 fn client_id_for_header(header_name: &'static str, header_value: &'static str) -> String {
511 let req = TestRequest::default()
512 .peer_addr("203.0.113.55:1000".parse().unwrap())
513 .insert_header((header_name, header_value))
514 .to_srv_request();
515 get_client_identifier(&req, &[]).unwrap()
516 }
517
518 #[test]
519 fn client_identifier_groups_api_key_transports() {
520 let configured = client_id_for_header("x-litellm-key", "gw-same-key");
521 let fallback = client_id_for_header("x-api-key", "gw-same-key");
522 let authorization_scheme = client_id_for_header("authorization", "ApiKey gw-same-key");
523 let authorization_raw = client_id_for_header("authorization", "gw-same-key");
524
525 assert_eq!(configured, fallback);
526 assert_eq!(configured, authorization_scheme);
527 assert_eq!(configured, authorization_raw);
528 }
529
530 #[test]
531 fn client_identifier_groups_rotated_credentials_into_one_bucket() {
532 let first = client_id_for_header("x-api-key", "gw-first-key");
536 let second = client_id_for_header("x-api-key", "gw-second-key");
537
538 assert_eq!(first, second);
539 assert_eq!(first, "ip:203.0.113.55");
540 }
541
542 #[test]
543 fn client_identifier_groups_jwt_and_api_key_guesses_from_same_ip() {
544 let api_key = client_id_for_header("x-api-key", "gw-some-key");
545 let jwt = client_id_for_header("authorization", "Bearer some.jwt.token");
546
547 assert_eq!(api_key, jwt);
548 assert_eq!(jwt, "ip:203.0.113.55");
549 }
550
551 #[test]
552 fn client_identifier_ignores_peer_port() {
553 let req_a = TestRequest::default()
554 .peer_addr("203.0.113.60:1000".parse().unwrap())
555 .insert_header(("x-api-key", "gw-same-key"))
556 .to_srv_request();
557 let req_b = TestRequest::default()
558 .peer_addr("203.0.113.60:2000".parse().unwrap())
559 .insert_header(("x-api-key", "gw-same-key"))
560 .to_srv_request();
561 assert_eq!(
562 get_client_identifier(&req_a, &[]),
563 get_client_identifier(&req_b, &[])
564 );
565 }
566
567 #[test]
568 fn client_identifier_falls_back_to_ip_without_auth() {
569 let req = TestRequest::default()
570 .peer_addr("203.0.113.70:1000".parse().unwrap())
571 .to_srv_request();
572
573 assert_eq!(
574 get_client_identifier(&req, &[]).as_deref(),
575 Some("ip:203.0.113.70")
576 );
577 }
578
579 #[test]
580 fn client_identifier_keeps_session_failures_in_ip_bucket() {
581 let req_a = TestRequest::default()
582 .peer_addr("203.0.113.80:1000".parse().unwrap())
583 .insert_header(("cookie", "session=session-a"))
584 .to_srv_request();
585 let req_b = TestRequest::default()
586 .peer_addr("203.0.113.80:1000".parse().unwrap())
587 .insert_header(("cookie", "session=session-b"))
588 .to_srv_request();
589 assert_eq!(
590 get_client_identifier(&req_a, &[]),
591 get_client_identifier(&req_b, &[])
592 );
593 assert_eq!(
594 get_client_identifier(&req_a, &[]).as_deref(),
595 Some("ip:203.0.113.80")
596 );
597 }
598
599 #[test]
600 fn client_identifier_uses_forwarded_client_only_for_trusted_peer() {
601 let trusted = vec!["192.0.2.10".to_string()];
602 let trusted_req = TestRequest::default()
603 .peer_addr("192.0.2.10:443".parse().unwrap())
604 .insert_header(("x-forwarded-for", "198.51.100.20, 192.0.2.10"))
605 .to_srv_request();
606 let untrusted_req = TestRequest::default()
607 .peer_addr("192.0.2.11:443".parse().unwrap())
608 .insert_header(("x-forwarded-for", "198.51.100.20"))
609 .to_srv_request();
610
611 assert_eq!(
612 get_client_identifier(&trusted_req, &trusted).as_deref(),
613 Some("ip:198.51.100.20")
614 );
615 assert_eq!(
616 get_client_identifier(&untrusted_req, &trusted).as_deref(),
617 Some("ip:192.0.2.11")
618 );
619 }
620
621 #[test]
622 fn client_identifier_without_transport_peer_is_untracked() {
623 let req = TestRequest::default()
624 .insert_header(("x-forwarded-for", "198.51.100.20"))
625 .to_srv_request();
626
627 assert_eq!(get_client_identifier(&req, &[]), None);
628 }
629
630 #[test]
631 fn auth_verification_precheck_only_applies_to_present_credentials() {
632 assert!(requires_auth_verification(&AuthMethod::Jwt(
633 "jwt-token".to_string()
634 )));
635 assert!(requires_auth_verification(&AuthMethod::ApiKey(
636 "api-key".to_string()
637 )));
638 assert!(requires_auth_verification(&AuthMethod::Session(
639 "session-id".to_string()
640 )));
641 assert!(!requires_auth_verification(&AuthMethod::None));
642 }
643
644 #[test]
645 fn readiness_uses_optional_header_auth() {
646 assert!(bypasses_header_auth("/health/ready", &AuthMethod::None));
647 assert!(!bypasses_header_auth(
648 "/health/ready",
649 &AuthMethod::ApiKey("present".to_string())
650 ));
651 assert!(bypasses_header_auth(
652 "/health",
653 &AuthMethod::ApiKey("ignored".to_string())
654 ));
655 }
656
657 #[test]
658 fn insert_request_context_stores_shared_extension_handle() {
659 let api_key_id = uuid::Uuid::new_v4();
660 let mut req = TestRequest::default().to_srv_request();
661
662 insert_request_context(&mut req, RequestContext::new().with_api_key(api_key_id));
663
664 let extensions = req.extensions();
665 let stored = extensions
666 .get::<SharedRequestContext>()
667 .expect("request context should be stored as a shared handle");
668 assert_eq!(stored.api_key_id(), Some(api_key_id));
669 assert!(extensions.get::<RequestContext>().is_none());
670 }
671
672 #[test]
673 fn forbidden_response_retains_authenticated_principal_context() {
674 let user_id = uuid::Uuid::new_v4();
675 let api_key_id = uuid::Uuid::new_v4();
676 let team_id = uuid::Uuid::new_v4();
677 let mut req = TestRequest::default().to_srv_request();
678 insert_request_context(
679 &mut req,
680 RequestContext::new()
681 .with_user(user_id, Some(team_id))
682 .with_api_key(api_key_id),
683 );
684
685 let response = forbidden_response::<actix_web::body::BoxBody>(req, "denied");
686 let extensions = response.request().extensions();
687 let context = extensions
688 .get::<SharedRequestContext>()
689 .expect("authenticated context should survive a 403 response");
690
691 assert_eq!(
692 context.user_id.as_deref(),
693 Some(user_id.to_string().as_str())
694 );
695 assert_eq!(context.api_key_id(), Some(api_key_id));
696 assert_eq!(context.team_id(), Some(team_id));
697 }
698
699 #[test]
700 fn build_request_context_excludes_sensitive_auth_headers() {
701 let mut req = TestRequest::default()
702 .insert_header(("authorization", "Bearer secret"))
703 .insert_header(("x-api-key", "sk-secret"))
704 .insert_header(("x-request-id", "req-123"))
705 .insert_header(("x-observable", "kept"))
706 .to_srv_request();
707
708 let context = build_request_context(&mut req);
709
710 assert_eq!(context.request_id, "req-123");
711 assert_eq!(
712 context.headers.get("x-observable").map(String::as_str),
713 Some("kept")
714 );
715 assert!(!context.headers.contains_key("authorization"));
716 assert!(!context.headers.contains_key("x-api-key"));
717 }
718
719 #[actix_web::test]
720 async fn authentication_unavailable_response_is_generic_server_error() {
721 let req = TestRequest::with_uri("/v1/chat/completions").to_srv_request();
722 let response = authentication_unavailable_response::<actix_web::body::BoxBody>(req);
723
724 assert_eq!(
725 response.status(),
726 actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
727 );
728 let body = actix_web::body::to_bytes(response.into_body())
729 .await
730 .expect("generic authentication error body should render");
731 let body: serde_json::Value = serde_json::from_slice(&body)
732 .expect("generic authentication error body should be valid JSON");
733 assert_eq!(
734 body["error"]["message"],
735 AUTHENTICATION_SERVICE_UNAVAILABLE_MESSAGE
736 );
737 let body = body.to_string();
738 for internal_detail in [
739 "Storage error",
740 "Database error",
741 "Redis error",
742 "Connection closed",
743 ] {
744 assert!(!body.contains(internal_detail));
745 }
746 }
747}