Move 17 library crates into crates/, CLI binary into bin/fxt, consolidate 10 test crates into testing/, split config crate from deployment config files. Root directory reduced from 38+ to ~17 directories. All Cargo.toml paths and build.rs proto refs updated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.6 KiB
Rust
108 lines
3.6 KiB
Rust
//! 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 `<token>`" 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<S: TokenStorage> {
|
|
/// Token manager for retrieving current access token
|
|
auth_manager: AuthTokenManager<S>,
|
|
}
|
|
|
|
impl<S: TokenStorage> AuthInterceptor<S> {
|
|
/// Create a new authentication interceptor
|
|
pub const fn new(auth_manager: AuthTokenManager<S>) -> Self {
|
|
Self { auth_manager }
|
|
}
|
|
}
|
|
|
|
impl<S: TokenStorage + 'static> Interceptor for AuthInterceptor<S> {
|
|
fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, 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 <token>"
|
|
let bearer_token = format!("Bearer {}", access_token);
|
|
|
|
// Parse as metadata value
|
|
let token_value = bearer_token.parse::<MetadataValue<_>>().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_owned(),
|
|
refresh_token: "test_refresh_token".to_owned(),
|
|
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());
|
|
}
|
|
}
|