micromegas_auth/axum.rs
1//! Axum middleware for HTTP authentication
2//!
3//! Provides authentication middleware for Axum HTTP services that:
4//! 1. Extracts request parts (headers, method, URI)
5//! 2. Validates using configured AuthProvider
6//! 3. Injects AuthContext into request extensions
7//! 4. Returns 401 Unauthorized on invalid credentials, 503 Service Unavailable
8//! when a provider's backing store is unreachable
9
10use crate::client_ip::resolve_client_ip;
11use crate::types::{AuthProvider, HttpRequestParts, ProviderUnavailable, RequestParts};
12use axum::{
13 extract::Request,
14 http::StatusCode,
15 middleware::Next,
16 response::{IntoResponse, Response},
17};
18use micromegas_tracing::prelude::*;
19use std::sync::Arc;
20
21/// Axum middleware for request-based authentication
22///
23/// This middleware extracts request parts (headers, method, URI),
24/// validates them using the provided AuthProvider, and injects the resulting
25/// AuthContext into the request extensions.
26///
27/// # Example
28///
29/// ```rust,ignore
30/// use axum::{Router, middleware};
31/// use micromegas_auth::axum::auth_middleware;
32/// use micromegas_auth::api_key::ApiKeyAuthProvider;
33/// use std::sync::Arc;
34///
35/// let auth_provider = Arc::new(ApiKeyAuthProvider::new(keyring));
36/// let app = Router::new()
37/// .layer(middleware::from_fn(move |req, next| {
38/// auth_middleware(auth_provider.clone(), req, next)
39/// }));
40/// ```
41pub async fn auth_middleware(
42 auth_provider: Arc<dyn AuthProvider>,
43 mut req: Request,
44 next: Next,
45) -> Result<Response, AuthError> {
46 // Resolved before building `HttpRequestParts`: axum's `ConnectInfo<SocketAddr>` extension is
47 // already on `req` for every service that enables it, same as `get_client_ip`'s existing
48 // HTTP callers rely on.
49 let client_ip = resolve_client_ip(req.headers(), req.extensions());
50
51 // Extract request parts for authentication
52 let parts = HttpRequestParts {
53 headers: req.headers().clone(),
54 method: req.method().clone(),
55 uri: req.uri().clone(),
56 client_ip,
57 };
58
59 // Validate request using auth provider
60 let auth_ctx = auth_provider
61 .validate_request(&parts as &dyn RequestParts)
62 .await
63 .map_err(|e| {
64 if e.downcast_ref::<ProviderUnavailable>().is_some() {
65 debug!("[auth_failure] {e}");
66 AuthError::Unavailable
67 } else {
68 warn!("[auth_failure] {e}");
69 AuthError::InvalidToken
70 }
71 })?;
72
73 // Log successful authentication (trace level to avoid noise on every request)
74 trace!(
75 "[auth_success] subject={} email={:?} issuer={} admin={}",
76 auth_ctx.subject,
77 auth_ctx.email,
78 auth_ctx.issuer,
79 auth_ctx.is_admin()
80 );
81
82 // SECURITY: Remove any client-provided auth headers to prevent spoofing
83 // These headers should only be trusted when set by the authentication layer
84 // The AuthContext in request extensions is the authoritative source
85 req.headers_mut().remove("x-auth-subject");
86 req.headers_mut().remove("x-auth-email");
87 req.headers_mut().remove("x-auth-issuer");
88 req.headers_mut().remove("x-allow-delegation");
89 req.headers_mut().remove("x-auth-is-admin");
90
91 // Inject auth context into request extensions for downstream handlers
92 req.extensions_mut().insert(auth_ctx);
93
94 // Continue to next middleware/handler
95 Ok(next.run(req).await)
96}
97
98/// Authentication errors for HTTP responses
99#[derive(Debug)]
100pub enum AuthError {
101 /// Token validation failed
102 InvalidToken,
103 /// A DB-backed auth provider could not reach its key store. Distinct from
104 /// `InvalidToken` so the client retries instead of treating the credential as
105 /// permanently rejected (`rust/telemetry-sink/src/http_event_sink.rs` classifies
106 /// `4xx` as terminal and `5xx` as retryable).
107 Unavailable,
108}
109
110impl IntoResponse for AuthError {
111 fn into_response(self) -> Response {
112 let (status, message) = match self {
113 AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"),
114 AuthError::Unavailable => {
115 (StatusCode::SERVICE_UNAVAILABLE, "Auth provider unavailable")
116 }
117 };
118
119 (status, message).into_response()
120 }
121}