micromegas_auth/
tower.rs

1//! Tower service layer for async authentication with tonic/gRPC.
2//!
3//! This module provides a tower service wrapper that integrates authentication
4//! into tonic gRPC services. It extracts request parts from gRPC metadata,
5//! validates them using an AuthProvider, and injects the AuthContext into request extensions.
6
7use crate::types::{AuthProvider, GrpcRequestParts, RequestParts};
8use futures::future::BoxFuture;
9use micromegas_tracing::prelude::*;
10use std::sync::Arc;
11use tonic::Status;
12use tower::Service;
13
14/// Async authentication service wrapper for tonic/gRPC.
15///
16/// This service wraps another tower service and adds authentication:
17/// 1. Extracts request parts from gRPC metadata
18/// 2. Validates request using the configured AuthProvider
19/// 3. Injects AuthContext into request extensions
20/// 4. Logs authentication success/failure
21///
22/// If no auth_provider is configured, requests pass through without authentication.
23///
24/// # Example
25///
26/// ```rust,no_run
27/// use micromegas_auth::api_key::{ApiKeyAuthProvider, parse_key_ring};
28/// use micromegas_auth::tower::AuthService;
29/// use std::sync::Arc;
30///
31/// # async fn example<S>(inner_service: S) -> anyhow::Result<()>
32/// # where
33/// #     S: tower::Service<http::Request<tonic::body::Body>> + Clone + Send + 'static,
34/// #     S::Response: 'static,
35/// #     S::Future: Send + 'static,
36/// #     S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
37/// # {
38/// // Create auth provider
39/// let keyring = parse_key_ring(r#"[{"name": "test", "key": "secret"}]"#)?;
40/// let auth_provider = Arc::new(ApiKeyAuthProvider::new(keyring));
41///
42/// // Wrap your service with authentication
43/// let auth_service = AuthService {
44///     inner: inner_service,
45///     auth_provider: Some(auth_provider as Arc<dyn micromegas_auth::types::AuthProvider>),
46/// };
47/// # Ok(())
48/// # }
49/// ```
50#[derive(Clone)]
51pub struct AuthService<S> {
52    /// The inner service to wrap
53    pub inner: S,
54    /// Optional authentication provider (None = no auth required)
55    pub auth_provider: Option<Arc<dyn AuthProvider>>,
56}
57
58impl<S> Service<http::Request<tonic::body::Body>> for AuthService<S>
59where
60    S: Service<http::Request<tonic::body::Body>> + Clone + Send + 'static,
61    S::Response: 'static,
62    S::Future: Send + 'static,
63    S::Error: Into<Box<dyn std::error::Error + Send + Sync>>,
64{
65    type Response = S::Response;
66    type Error = Box<dyn std::error::Error + Send + Sync>;
67    type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
68
69    fn poll_ready(
70        &mut self,
71        cx: &mut std::task::Context<'_>,
72    ) -> std::task::Poll<Result<(), Self::Error>> {
73        self.inner.poll_ready(cx).map_err(Into::into)
74    }
75
76    fn call(&mut self, req: http::Request<tonic::body::Body>) -> Self::Future {
77        let clone = self.inner.clone();
78        let mut inner = std::mem::replace(&mut self.inner, clone);
79        let auth_provider = self.auth_provider.clone();
80
81        Box::pin(async move {
82            if let Some(provider) = auth_provider {
83                let (mut parts, body) = req.into_parts();
84
85                // Extract request parts for validation
86                let request_parts = GrpcRequestParts {
87                    metadata: tonic::metadata::MetadataMap::from_headers(parts.headers.clone()),
88                };
89
90                // Validate request
91                match provider
92                    .validate_request(&request_parts as &dyn RequestParts)
93                    .await
94                {
95                    Ok(auth_ctx) => {
96                        info!(
97                            "authenticated: subject={} email={:?} issuer={} audience={:?} admin={}",
98                            auth_ctx.subject,
99                            auth_ctx.email,
100                            auth_ctx.issuer,
101                            auth_ctx.audience,
102                            auth_ctx.is_admin
103                        );
104
105                        // SECURITY: Remove any client-provided auth headers to prevent spoofing
106                        // These headers are only set by the authentication layer
107                        parts.headers.remove("x-auth-subject");
108                        parts.headers.remove("x-auth-email");
109                        parts.headers.remove("x-auth-issuer");
110                        parts.headers.remove("x-allow-delegation");
111
112                        // Inject auth context into gRPC metadata headers
113                        parts.headers.insert(
114                            "x-auth-subject",
115                            http::HeaderValue::from_str(&auth_ctx.subject)
116                                .expect("valid user id header"),
117                        );
118                        if let Some(email) = &auth_ctx.email {
119                            parts.headers.insert(
120                                "x-auth-email",
121                                http::HeaderValue::from_str(email).expect("valid email header"),
122                            );
123                        }
124                        parts.headers.insert(
125                            "x-auth-issuer",
126                            http::HeaderValue::from_str(&auth_ctx.issuer)
127                                .expect("valid issuer header"),
128                        );
129                        parts.headers.insert(
130                            "x-allow-delegation",
131                            http::HeaderValue::from_str(&auth_ctx.allow_delegation.to_string())
132                                .expect("valid allow_delegation header"),
133                        );
134
135                        parts.extensions.insert(auth_ctx);
136                        let req = http::Request::from_parts(parts, body);
137                        inner.call(req).await.map_err(Into::into)
138                    }
139                    Err(e) => {
140                        warn!("authentication failed: {e}");
141                        Err(Box::new(Status::unauthenticated("invalid token"))
142                            as Box<dyn std::error::Error + Send + Sync>)
143                    }
144                }
145            } else {
146                inner.call(req).await.map_err(Into::into)
147            }
148        })
149    }
150}