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}