Skip to main content

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}