//! gRPC authentication interceptor //! //! Adds JWT Bearer tokens to all outgoing gRPC requests using tonic interceptors. use tonic::{metadata::MetadataValue, service::Interceptor, Request, Status}; use super::token_manager::{AuthTokenManager, TokenStorage}; /// gRPC authentication interceptor /// /// Automatically adds "Authorization: Bearer ``" header to all outgoing /// gRPC requests. If no valid token is available, requests proceed without /// authentication (allowing login requests to work). #[derive(Clone)] pub struct AuthInterceptor { /// Token manager for retrieving current access token auth_manager: AuthTokenManager, } impl AuthInterceptor { /// Create a new authentication interceptor pub const fn new(auth_manager: AuthTokenManager) -> Self { Self { auth_manager } } } impl Interceptor for AuthInterceptor { fn call(&mut self, mut request: Request<()>) -> Result, Status> { // Get cached access token synchronously (safe for gRPC interceptor) let token = self.auth_manager.get_cached_access_token(); if let Some(access_token) = token { // Format as "Bearer " let bearer_token = format!("Bearer {}", access_token); // Parse as metadata value let token_value = bearer_token .parse::>() .map_err(|e| { tracing::error!("Failed to parse JWT as metadata value: {}", e); Status::internal("Invalid token format") })?; // Add to request metadata request.metadata_mut().insert("authorization", token_value); tracing::trace!("Added JWT Bearer token to gRPC request"); } else { tracing::trace!("No valid access token - request proceeding without authentication"); } Ok(request) } } #[cfg(test)] mod tests { use super::*; use crate::auth::token_manager::{InMemoryTokenStorage, TokenInfo}; use std::time::{SystemTime, UNIX_EPOCH}; #[tokio::test(flavor = "multi_thread")] async fn test_interceptor_adds_token() { let storage = InMemoryTokenStorage::new(); let manager = AuthTokenManager::new(storage); let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); let token_info = TokenInfo { access_token: "test_access_token".to_string(), refresh_token: "test_refresh_token".to_string(), expires_at: now + 3600, }; manager.set_tokens(token_info).await.unwrap(); let mut interceptor = AuthInterceptor::new(manager); let request = Request::new(()); let result = interceptor.call(request); assert!(result.is_ok()); let request = result.unwrap(); let auth_header = request.metadata().get("authorization"); assert!(auth_header.is_some()); let auth_value = auth_header.unwrap().to_str().unwrap(); assert_eq!(auth_value, "Bearer test_access_token"); } #[tokio::test(flavor = "multi_thread")] async fn test_interceptor_without_token() { let storage = InMemoryTokenStorage::new(); let manager = AuthTokenManager::new(storage); let mut interceptor = AuthInterceptor::new(manager); let request = Request::new(()); let result = interceptor.call(request); assert!(result.is_ok()); let request = result.unwrap(); let auth_header = request.metadata().get("authorization"); assert!(auth_header.is_none()); } }