Files
foxhunt/bin/fxt/src/auth/interceptor.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
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>
2026-02-25 11:56:00 +01:00

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());
}
}